Безопасный запуск

Мы можем тщательно написать программу, чтобы она правильно работала, но всё окажется бесполезно, если она не сможет корректно запуститься.

Машина, которая не заводится, никуда не годится, даже если она ездит как Rolls-Royce.

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

Предвыполнение

Типичная программа состоит из некоторого количества библиотечных пакетов P, Q, R и т. д. плюс главная подпрограмма M. При запуске программы пакеты предвыполняются, а затем вызывается главная подпрограмма. Предвыполнение пакета заключается в создании различных сущностей, объявленных в пакете на верхнем уровне. Но это не касается сущностей внутри подпрограмм, поскольку они создаются лишь в момент вызова этих подпрограмм.

Вернемся к примеру со стеком из главы «Безопасная Архитектура». Вкратце он выглядит так:

package Stack is
   procedure Clear;
   procedure Push(X: Float);
   function Pop return Float;
end Stack;

package body Stack is
   Max: constant := 100;
   Top: Integer range 0 .. Max := 0;
   A: array (1 .. Max) of Float;

   ... -- подпрограммы Clear, Push и Pop
end Stack;

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

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

Max: constant := Some_Function;

Top: Integer range 0 .. Max := 0;

A: array (1 .. Max) of Float;

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

Max: Integer;

Top: Integer range 0 .. Max := 0;

A: array (1 .. Max) of Float;

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

Следует также отметить, что мы инициализируем переменную Top нулем, чтобы пользователю не пришлось вызывать Clear перед первым вызовом Push или Pop.

Также мы можем добавить в тело пакета код для явной инициализации, например так:

package body Stack is
   Max: constant := 100;
   Top: Integer range 0 .. Max := 0;
   A: array (1 .. Max) of Float;

   ... -- подпрограммы Clear, Push и Pop

begin   -- далее явная инициализация
   Top := 0;
end Stack;

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

Может показаться, что всегда предоставлять значения по умолчанию для все переменных — это хорошая идея. В нашем примере значение «0» — весьма подходящее и соответствует состоянию стека после исполнения Clear. Но в некоторых случаях нет очевидного подходящего значения для инициализации. В этих случаях, использовать произвольное значение не разумно, поскольку это может затруднить обнаружение реальных ошибок. Мы еще вернемся к этому вопросу при обсуждении языка SPARK в заключительной главе.

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

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

package P is
   function F return Integer;
   X: Integer := F;      -- возбудит Program_Error
end;

где тело F конечно находится в теле пакета P. Невозможно вызвать F, чтобы получить начальное значение X до того, как тело F будет предвыполнено, поскольку тело F может ссылаться на переменную X или любую другую переменную, объявленную после X, ведь они все еще не инициализированы. Поэтому в Аде это приведет к возбуждению исключения Program_Error. В языке C подобные ошибки приведут к непредсказуемому результату.

Директивы компилятору, связанные с предвыполнением

Внутри одного модуля компиляции предвыполнение происходит в порядке объявления сущностей.

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

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

package P is
   function F return Integer;
end P;

package body P is
   N: Integer := какое-то_значение;
   function F return Integer is
   begin
      return N;
   end F;
end P;


with P;
package Q is
   X: Integer := P.F;
end Q;

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

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

with P;
pragma Elaborate_All (P);

package Q is
   X: Integer := P.F;
end Q;

Отметим, что All в наименовании инструкции подчеркивает ее транзитивный характер. Как результат, предвыполнение пакета P (и всех его зависимостей) произойдет до предвыполнения кода Q.

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

Здесь читатель может поинтересоваться, возможно ли решить проблему порядка предвыполнения, введя дополнительные простые правила в язык. Например, как если бы инcтрукция Elaborate_Body присутствовала всегда. К сожалению, это не сработает, поскольку полностью запретит взаимно рекурсивные пакеты (т. е. когда тело пакета P1 имеет спецификатор with P2; а P2 соответственно with P1).

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

Динамическая загрузка

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

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

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

Подход, который можно предложить в Аде, использует механизм теговых типов для динамической загрузки. Смысл в том, что существующий код использует надклассовый тип (такой как Geometry.Object'Class) для вызова операций (таких как Area) любых конкретных новых типов (например пятиугольник, шестиугольник и пр.), и при этом создание новых типов не требует перекомпиляции существующего кода. Этого мы кратко касались в главе «Безопасное ООП». Этот механизм полностью безопасен с точки зрения строгой типизации.

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