Помощь - Поиск - Пользователи - Календарь
Полная версия: Низкоуровневая совместимость между версиями
Форум «Всё о Паскале» > Разработка ПО, алгоритмы, общие вопросы > Общие вопросы разработки программ
OCTAGRAM
Представляю свой перевод широко известного (в узких кругах, которые неплохо бы расширить) доклада

Forman I.R., Conner M.H., Danforth S.H., Raper L.K. Release-to-Release Binary Compatibility and the Correctness of Separate Compilation // OOPSLA ’95 Conference Proceedings. New York: ACM, 1995. P. 426–438. doi:10.1145/217838.217880

После того, как в 1995м доклад был представлен на конференции, двое из его авторов в 1998м выпустили ещё более широко известную книгу «Putting Metaclasses to Work» (DJVU, PDF, ZIP с Java-симуляцией), и этот доклад с небольшими изменениями составил одноимённую главу 11. Этой книгой вдохновлялся создатель Python, на что он явно указывает в истории развития языка. На эту книгу часто можно видеть ссылки при обсуждении метаклассов.

Но нужно понимать, что объектная модель с множественным наследованием и метаклассами — это всего лишь отблеск SOM, а начиналось всё с другого. С RRBC. Я перевожу это как «Низкоуровневая совместимость между версиями». Возможно, неуклюже, но лучше пока не придумал.

Интересующиеся могут найти разную связанную с темой информацию и средства разработки по адресу http://octagram.name/pub/somobjects/

Доклад состоит из 12 разделов, включает в себя 9 рисунков и 1 таблицу. Раздел «Полнота множества трансформаций (Completeness of a Set of Transformations)» остался без перевода. В этом разделе несколько страниц формальных манипуляций, а по сути ничего особенно нового.

Низкоуровневая совместимость между версиями
и
корректность раздельной компиляции

Release-to-Release Binary Compatibility
and the
Correctness of Separate Compilation


Айра Форман, Майкл Коннэр, Скотт Дэнфорт, Ларри Рэйпер
Ira R. Forman, Michael H. Conner, Scott H. Danforth, Larry K. Raper


IBM Object Technology Products
Перевод: Иван Левашев


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

Модель системных объектов (System Object Model, SOM) создана быть таковой. Следующая секция даёт обзор SOM, после которого мы вернёмся к проблеме написания и поддержке совместимых на низком уровне библиотек классов. Этот доклад представляет решение этой проблемы, воплощённое в SOM.

Оглавление
OCTAGRAM
Модель SOM

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

На диаграммах классовых иерархий в этом докладе применяется соглашение, что метаклассы обозначены трёмя концентрическими окружностями, обычные классы (то есть, классы, которые не метаклассы) обозначены двумя концентрическими окружностями, а обычные объекты (то есть, объекты, которые не классы) обозначены одиночными окружностями. Начальное состояние условной программы на SOM обозначено на рисунке 1. Всего 4 объекта: SOMObject (класс), SOMClass (метакласс), Dog (обычный класс), и Rover (обычный объект). Между объектами вводится два вида отношений, которые необходимо понять.

Изображение
Рисунок 1. Примеры различных объектов SOM.

Во–первых, отношение экземпляр-чего между объектами и классами изображено штриховой стрелкой от объекта к его классу. По обстоятельствам, также изображается обратное отношение класс-чего. SOMObject — это экземпляр SOMClass, а SOMClass — это экземпляр самого себя. Класс объекта требуется, поскольку объект поддерживает только те методы, которые определены его классом (то есть, методы, которые класс привносит или наследует).

Во–вторых, отношение между классами, называемое отношением подкласс-чего, изображено сплошной стрелкой от класса ко всем его родителям. SOMClass — это подкласс SOMObject. У SOMObject нет родителей. SOMObject привносит методы, которые должны поддерживаться всеми объектами в SOM. Будучи подклассом SOMObject, SOMClass — это объект, но дополнительно он привносит методы, которые должны поддерживаться всеми классами. Например, SOMClass привносит метод somNew, кототорый создаёт экземпляр класса. Также привносятся методы, отвечающие за создание и изменение таблицы методов экземпляра. Абсолютно все метаклассы в SOM наследуются от SOMClass. (Похожее устройство классов также применяется в других объектных моделях, например, CLOS и Dylan.) SOMClass и SOMObject — это два класса в ядре SOM.

Интерфейсы объектов SOM описываются при помощи IDL, языка описания интерфейсов объектов (Interface Definition Language), определённого стандартом Общей архитектуры брокера объектных запросов (Common Object Request Broker Architecture, CORBA) рабочей группы Object Management Group (OMG). SOM IDL — это совместимая с CORBA версия IDL, используемая, чтобы описания классов SOM были рядом с определениями интерфейсов объектов. (Таким образом, интерфейс класса описывается обычным IDL, а SOM IDL добавляет дополнительную информацию о реализации.) В составе SOMobjects Toolkit есть инструменты, называемые эмиттерами, которые транслируют SOM IDL в привязки для выбранного языка программирования к объектам соответствующих классов (то есть, для разработчиков на языке Си это значит, что эмиттер создаёт заголовочные файлы как для использования класса, так и для реализации). Вдобавок, существуют компиляторы C++, производящие код, непосредственно использующий SOM.

Ниже представлена базовая структура определения IDL для объектного интерфейса под названием Dog. В то же время, это описание SOM IDL класса Dog, поддерживающего этот интерфейс. #ifdef и #endif (которые для простоты опущены в последующих примерах) — это часть языка IDL, и используются для сокрытия секции реализации класса SOM от компиляторов, не поддерживающих SOM IDL.

interface Dog : SOMObject {
<method and attribute declarations here>
#ifdef __SOMIDL__
implementation {
metaclass = SOMClass;
<instance variable declarations here>
};
#endif
};


В этом примере интерфейс Dog наследуется от интерфейса SOMObject, и в то же время класс Dog объявлен как подкласс SOMObject. CORBA и SOM поддерживают множественное наследование; дополнительные родители Dog могут быть указаны вместе с SOMObject через запятую. Собственно методы и поля Dog не представляют интереса в текущем контексте. Как показано в этом примере, секция реализации может явным образом указывать метакласс, связанный с классом объектов, поддерживающих определяемый интерфейс. Эта связь, однако, не всегда прямая. По причинам, которые станут ясны позже, настоящий класс описанного SOM IDL класса — в общем случае подкласс указанного метакласса.
OCTAGRAM
Проблема совместимости библиотек

В SOM существует явное разграничение между программным интерфейсом (Application Programming Interface, API) и низкоуровневым (Application Binary Interface, ABI). API — это интерфейс, которым пользуется разработчик, а ABI — это набор соглашений, от которых зависит исполнение программ. Совместимость на уровне API значит, что приложения могут быть успешно перекомпилированы; совместимость на уровне ABI значит, что скомпилированные приложения продолжают успешно работать.

Давайте рассмотрим затруднение поставщика программного кода, который продаёт программную библиотеку; покупатели поставщика библиотеки используют библиотеку для разработки приложений. Поставщик библиотеки даёт покупателям описание API библиотеки (например, заголовочные файлы или OMG IDL) и скомпилированную библиотеку в форме динамически компонуемой библиотеки (Dynamically Linked Library, DLL). Интерфейс (скомпилированной) DLL называется ABI. Преимущество использования DLL (и для поставщика библиотеки, и для создателя приложения) — в том, что несколько приложений могут выполняться с одной копией DLL, что значительно уменьшает требования к оперативной памяти и улучшает время отклика. Проблемы начинаются, когда поставщик библиотеки пытается развивать библиотеку. Поставщик библиотеки должен поддерживать низкоуровневую совместимость между версиями, потому что создатель приложения не может перекомпилировать уже разосланные копии приложения.

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

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

Теперь немного абстракций; покажем на примере, как C++ не справляется с поддержкой низкоуровневой совместимости между версиями. На рисунке 2 изображена ситуация, при которой от класса X (из библиотеки классов) наследуется класс Y в приложении. С библиотекой версии 1.0 приложение работает великолепно; в частности, можно вызвать fmethod у iY, экземпляра класса Y. Теперь рассмотрим, что происходит, когда поставщик библиотеки делает на первый взгляд безвредное изменение, добавляя метод (hmethod) к классу X, который вызывается из fmethod. Теперь, поскольку приложение собрано с использованием старого определения библиотеки, в таблице методов Y указатель на gmethod ожидается во второй ячейке. Однако, когда у iY вызывается fmethod, он впоследствии пытается вызвать hmethod, который в классе X находится во второй ячейке таблицы методов. Как результат, вместо hmethod вызывается gmethod.

Изображение
Рисунок 2. Несовместимость, созданная типичным компилятором C++.

Эта ситуация до боли знакома разработчикам на C++. Коварство проблемы — в том, что она выглядит как проблема в классе X, который называется базовым классом в C++. Из-за этого и других подобных примеров возник миф «проблемы хрупкого базового класса». Всю вину свалили на бедный базовый класс, уводя внимание от настоящей проблемы: комбинация компилятора и компоновщика не поддерживает должным образом наследование поперёк низкоуровневого интерфейса.
OCTAGRAM
Определение низкоуровневой совместимости между версиями

Конструирование эффективного определения низкоуровневой совместимости между версиями — непростая задача. Считайте это лобовой атакой на проблему. Пусть

A ○○ L значит, что приложение A совмещено с библиотекой L
и пусть

SAT(P, S) значит, что программа P удовлетворяет спецификации S
где P, A и L обозначают скомпилированные модули.

Наивное определение 1. L1 — это RRBC-преемник L0, если
SAT(A ○○ L0, S) влечёт SAT(A ○○ L0, S)
для всех разумных функциональных спецификаций S и для всех приложений A, использующих библиотеку L.

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

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

Цель этой инженерной дисциплины может быть выражена так:

Только изменения в приложении требуют перекомпиляции


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

С точки зрения наивного определения, эти трансформации производят RRBC-совместимых преемников. Но поскольку наивное определение не является формальным, каждая трансформация должна быть обоснована отдельно. Вдобавок, перечисления трансформаций не достаточно в этой дисциплине. Поскольку нужно приспособиться работать со скомпилированными библиотеками, нужно потребовать, чтобы сама технология связки приложений с библиотеками поддерживала трансформации. Далее будет показано, что для процедурного программирования текущие компоновщики адекватны, но для объектно-ориентированного программирования только SOM поддерживает наиболее полный набор трансформаций.
OCTAGRAM
Процедурное программирование

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

При таком ABI проблема низкоуровневой совместимости между версиями сводится к вопросу:

Каждая ли процедура новой библиотеки является более полным воплощением предшественника?


В нашей инженерной дисциплине это приводит к появлению пяти трансформаций:

Трансформация 0: Процедура может быть переписана с нуля для лучшей производительности при неизменном интерфейсе. (Заметим, что мы не принимаем во внимание патологические случаи из жизни, когда более быстрая реализация перестаёт соответствовать спецификации.)
Трансформация 1: Область применения процедуры может быть расширена на входные значения, которые прежде приводили к отказу или невозврату управления (бесконечный цикл или тупик (deadlock)).
Трансформация 2: В системах, где соглашение о вызове содержит указание на количество аргументов, количество аргументов процедуры может быть увеличено.
Трансформация 3: Добавление новых процедур.
Трансформация 4: Удаление непубличных процедур.

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

Изображение
Рисунок 3. Процедурные библиотеки развиваются безопасно, добавляя новые процедуры.
OCTAGRAM
Объектно-ориентированное программирование

Богатство объектно-ориентированного программирования приводит к появлению новых граней ABI. Помимо ABI процедурных библиотек, ООП приложения наследуют классы от классов библиотек (см. рисунок 4). Новые аспекты проблем приводят к созданию улучшенной инженерной дисциплины, занимающейся низкоуровневой совместимостью между версиями библиотек классов и, как следствие, непрерывным функционированием зависимых приложений.

В нашей инженерной дисциплине это требует наличия следующих трансформаций:

Трансформация 5: Добавление новых полей в объекты
Трансформация 6: Добавление новых методов в классы
Трансформация 7: Вставка новых классов в иерархию
Трансформация 8: Миграция родительского класса вниз по иерархии классов
Трансформация 9: Миграция метода вверх по иерархии классов
Трансформация 10: Удаление непубличных классов.
Трансформация 11: Удаление непубличных методов
Трансформация 12: Удаление непубличных полей
Трансформация 13: Переупорядочивание методов класса
Трансформация 14: Переупорядочивание полей объекта
Необходимость в этих дополнительных трансформациях напрямую продиктована разрешением наследования поперёк ABI. SOM спроектирована так, чтобы поддерживать эти дополнительные трансформации.

Изображение
Рисунок 4. Классы приложения наследуются от классов библиотеки, которая развивается, добавляя новые классы.
OCTAGRAM
Когда классы — это полноценные объекты

SOM позволяет и поощряет определения и явное использование метаклассов. С учётом этого более широкого ABI наша инженерная дисциплина требует ещё одну дополнительную трансформацию.

Трансформация 15: Класс класса (то есть, ограничение на метакласс) может мигрировать вниз по иерархии классов.

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

Рассмотрим следующий простой пример, изображённый на рисунке 5. На этом рисунке X — это экземпляр XMeta; мы принимаем, что XMeta поддерживает метод bar и что X поддерживает метод foo, содержащий выражение bar(class(self)). Таким образом, метод foo вызывает метод на классе объекта, у которого вызван foo. Теперь посмотрим, что случится, если X наследуется Y, классом, у которого в SOM IDL явно указан метакласс, как на рисунке 5. Если бы иерархия классов образовывалась как на рисунке 5, вызов foo на экземпляре Y не сработал, поскольку YMeta не поддерживает bar. Эта ситуация называется несовместимостью метаклассов.

interface X {
...
void foo();
implementation{
metaclass = XMeta
};
};
where
foo()
{...
bar(class(self));
...};

interface Y:X {
...
implementation{
metaclass = YMeta
...


Изображение
Рисунок 5. Пример несовместимости метакласса.

SOM не позволяет создавать иерархии с несовместимостями метаклассов. Вместо этого, SOM создаёт производные метаклассы, которые предотвращают появление этой проблемы. Настоящая иерархия классов SOM, которая образуется для Y, изображена на рисунке 6, где SOM автоматически создала метакласс DerivedMetaclass; это гарантирует, что вызов foo на экземплярах Y сработает. Этот пример показывает, что оператор metaclass в SOM IDL обрабатывается как ограничение на итоговый метакласс. Производный метакласс — это в некотором смысле минимальный метакласс, удовлетворяющий ограничениям совместимости метаклассов.

Эта ситуация называется проблемой совместимости метакласса. В SOM нет такой проблемы; в ситуации, когда явно объявленный метакласс метакласс не совместим с родителями класса, конструируется соответствующий производный метакласс. Поскольку конструирование классов — это динамическая активность в SOM, класс производится уже во время выполнения без необходимости предварительного описания в IDL.

Сейчас все языки должны решать проблему несовместимости метакласса. Например, C++ решает проблему тем, что в нём нет метаклассов. Smalltalk решает проблему тем, что не позволяет явно указывать метакласс. CLOS содержит правило для проверки совместимости во время создания класса, исключающее возможность несовместимости метакласса (тем, что неудача при проверке совместимости приведёт к ошибке времени исполнения). Только SOM обрабатывает несовместимость метаклассов, не ограничивая разработчика.

Изображение
Рисунок 6. Пример произведённого метакласса.
OCTAGRAM
Как произведённые метаклассы помогают скомпилированным библиотекам

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

Рисунок 7 содержит конкретный пример. Приложение и две библиотеки в верхней половине диаграммы корректно работают вместе. Если библиотека A в ходе развития приобретает новый метакласс (что должно быть допустимо, поскольку перемещает ограничение на метакласс вниз), получается несовместимость метаклассов, изображённая на рисунке 5.

Изображение
Рисунок 7. Пример несовместимости метакласса, возникающей при использовании библиотек.

Рисунок 8 показывает, как выглядит нижняя часть рисунка 7 при создании ядром SOM. Этот пример поясняет ещё вот что. Несовместимость метаклассов может возникать между библиотеками. Разработчик метакласса не может знать, как будут метаклассы использованы в приложениях. Без автоматически произведённых метаклассов разработчик приложения не может избежать этой ситуации. Кроме того, в «Composition of Before/After Metaclasses in SOM» показано, насколько может быть полезно составлять метаклассы.

Изображение
Рисунок 8. SOM предотвращает несовместимость метаклассов.
OCTAGRAM
Сравнение поддержки в различных объектных моделях

Теперь, при выборе системы разработки, в которой будут создаваться скомпилированные библиотеки, нужно учесть, какие трансформации поддерживаются. Таблица 1 даёт сравнение для нескольких объектных моделей1. Компиляторы Smalltalk и C++ — обычные, в то время, как Delta/C++ относится к компилятору C++ от Silicon Graphics, а OBI относится к исследовательской работе Sun Microsystems.

В таблице 1 √ значит, что трансформация поддерживается, а X — что нет. В случаях, когда трансформация не имеет смысла применительно к этой технологии, в таблице 1 находится «n/a».

Таблица 1. Сравнение поддержки скомпилированных библиотек классов
Изображение

1 Мы исключили Microsoft COM, поскольку это интерфейсная, а не объектная модель, и её ABI не позволяет наследование между библиотекой и приложением. Если применить нашу технику анализа к COM, можно увидеть, что она поддерживает только Трансформации 0, 1, 3, 4, что ставит её в категорию процедурного программирования, а не объектно-ориентированного.
a Трансформация поддерживается для функций в скомпилированном ЛИСП, но не в обобщённых методах в CLOS.
b Из-за перегрузки методов в C++, в этой ячейке формально получается X, так как добавление нового параметра определяет новый метод. Может показаться, что это не проблема, так как перегрузка позволяет множеству методов иметь одинаковое имя. Однако, в связи с этим ответ для процедур отличается от методов.
c Однако, SOM поддерживает процедуры и методы, у которых определено переменное количество аргументов.
d В компилируемом Smalltalk добавление полей требует перекомпиляции приложений.
e Текущие реализации Java не поддерживают такое, но новые спецификации ясно требуют, чтобы все наши трансформации поддерживались.
f Следует заметить, что OBI — это исследовательский проект, имеющий дополнительную цель в поддержке множественных версий класса.
g Поскольку это подход C++, нужно удостовериться, что непубличные элементы не были сделаны видимыми через указание дружбы.
h Objective-C поддерживает интерфейс прямого доступа к полям, тем самым делая в этом поле X. Брэд Кокс сообщил нам, что мудрые разработчики на Objective-C не пользуются этим средством (и тем самым ставят в этой ячейке √).


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

Наша презентация была неформальной; например, мы не определили критерии для полного множества трансформаций, которые сохраняют совместимость (впрочем, и само сохранение совместимости не было формально определено). Обычно неполнота трансформаций влечёт другие недостатки. Но это не коснулось проблемы развития библиотек классов. Ясно, что SOM не полон, так как Трансформация 2 не поддерживается. Но, как было указано выше, это не серьёзное препятствие для развития библиотеки классов.
OCTAGRAM
Практика

Описанная здесь технология SOM — это больше, чем теория; она активно применялась и в IBM, и в сообществе OS/2 более трёх лет. Многочисленные приложения для OS/2 и системные средства, такие как IBM LAN Server, MMPM/2, Ultimail, IBM Works и даже OS/2 Workplace Shell (пользовательский интерфейс OS/2) созданы с использованием технологии SOM. Даже такие умеренного размера приложения, как эти, содержат дюжины, а иногда сотни классов SOM. Эти приложения были разработаны независимо и потом развивались, даже когда сам SOM менялся.

Рисунок 9 показывает ход истории для технологии SOM и операционной системы OS/2. Дебют SOM 1.0 состоялся в апреле 1992; он был ключевой частью первого 32-битного выпуска OS/2 2.0. Хотя версия SOM 1.0 поддерживала только примитивное одиночное наследование, многие из RRBC возможностей, описанные в этом докладе, уже присутствовали. Именно это изначальное присутствие возможностей RRBC позволило технологии развиться.

Версия SOM 2.0 совершила значительный рывок с точки зрения новых возможностей. Это была первая попытка среди всех коммерческих продуктов поддержать зарождающийся стандарт CORBA 1.1. Для SOM это значило переход на новый язык определения для SOM интерфейсов, расширения собственно базовой объектной модели (например, множественное наследование) и изменения в инфраструктуре, чтобы поддерживать распространение через брокер объектных запросов (Object Request Broker, ORB), известный как DSOM. Поскольку циклы продуктов OS/2 и SOM были совершенно независимы, SOM 2.0 появился в независимо лицензируемом продукте под названием SOMobjects Developer Toolkit. В дополнение к базовым инструментам разработки для SOM этот инструментарий также включал широкий набор каркасов для приложений и демонстрационных программ. Будучи установлен на любой операционной системе OS/2 2.0 или 2.1, инструментарий SOM полностью замещал более раннюю версию SOM. Так что, даже если никакое из приложений SOM для OS/2 ещё не воспользовалось преимуществами новых возможностей SOM 2.0, все каркасы, сделанные на SOM, продолжали работать без перекомпиляции, в то время как только что созданные каркасы, использующие SOM 2.0, выполнялись рядом в той же среде.

Изображение
Рисунок 9. Развитие SOMobjects Toolkit и OS/2 Workplace Shell.

В октябре 1994 версия SOMobjects Toolkit 2.1 была выпущена в общий доступ; этот выпуск SOM поправил некоторые обнаруженные дефекты (разработчики SOM не более особенные, чем другие) и во многом улучшил производительность. В это время циклы продуктов OS/2 Warp и SOM совпали более удачно, и SOM 2.1 был включён как внутренний элемент Warp. Важно заметить, что разработка OS/2 Warp Workplace Shell началась (в Бока Ратон, Флорида) с использованием ядра SOM 2.0, в то время как команда разработчиков SOM в Остине, Штат Техас, закончила версию 2.1. Только в последние дни подготовки Warp (июль 1994) команда разработчиков Workplace Shell получила новое ядро SOM 2.1. Благодаря RRBC в SOM, они могли тестировать без перекомпиляции; давая разработчикам SOM возможность действовать независимо, что было необходимо для удовлетворения требований к продукту, не влияя на график разработчиков Workplace Shell.

Этот опыт — один из многих похожих опытов, которые можно привести в качестве эмпирического подтверждения пакетной технологии SOM. Но более важно то, что благодаря упору на RRBC в SOM такая разработка независимо поставляемых скомпилированных библиотек классов должна считаться нормой.
OCTAGRAM
Заключение

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

Благодарности

Мы хотели бы поблагодарить Liane Acker, Arindam Banerji, Ravi Condamoor, Nissim Francez, Kevin Greene, Duane Hughes, John Irwin, Shmuel Katz, Vinoj Kumar, John Lamping, Simon Nash, Kim Rochat, Cun Xiao и рецензентов OOPSLA за их комментарии. В дополнение, мы бы хотели поблагодарить Brad Cox, Ted Goldstein и Andy Palay за их сотрудничество.
OCTAGRAM
Послесловие от переводчика

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

Во-первых, для разработчиков на Delphi нужно сделать пояснение, что в Delphi все метаклассы образуют теневое дерево, повторяющее дерево наследования обычных классов. К каждому метаклассу можно обратиться, указав "class of ...". Так, type System.TClass = class of System.TObject. Каждый раз, когда описывается новый класс, на самом деле, создаётся два, обычный и теневой. Все простые методы добавляются в обычный класс, а все так называемые классовые методы — в метакласс. Однако, в Delphi нет аналога полей метакласса. class var в самых новых версиях Delphi — это синтаксический сахар для глобальных переменных. В Delphi System.TClass не является дочерним классом System.TObject, и переменую типа System.TClass нельзя присвоить переменной System.TObject. И, конечно, нельзя явно указать метакласс. Таким образом, метаклассы есть, но они герметично изолированы от просто классов. Это называется «неявными метаклассами».

Язык Ada в этом отношении ещё менее развит, чем Delphi. Аналогом System.TClass является Ada.Tags.Tag. Кроме запроса имени, исследования иерархии и установления родства, максимум полезного, что можно сделать, — это использовать виртуальный конструктор при помощи Ada.Tags.Generic_Dispatching_Constructor, однако если виртуальный конструктор даже и не собирается создавать никакой объект, а бросит исключение, перед этим чего-нибудь поделав с аргументами, то уже получается аналог классовых методов. Только это отличает тэги от просто RTTI. Примерно на таком же уровне поддержка метаклассов в Java и .NET. Там классы представлены некоторыми объектами, но классовые методы не являются методами этого объекта.

В Objective-C, как и в Smalltalk, метаклассы неявные, однако, в отличие от Delphi, метаклассы имеют общий корень с обычными объектами. Нет определённости, однако, каким образом ядро Objective-C создаёт метаклассы. Оно может создавать теневую иерархию метаклассов параллельно обычной иерархии, а может производить метаклассы для каждого класса от одного родителя. Метаклассы в Objective-C — это вообще слабо изученная тема. Интересующихся могу направить почитать статью «Метаклассы в Objective-C». Они существуют на уровне ядра Objective-C, но не на уровне языка программирования. Выглядит так, как будто разработчики языка испытали потребность добавить классовые методы, и переиспользовали для этого механизм вызова методов, как у обычных объектов, тем самым положив начало метаклассам, но не были готовы развить эту тему и так и зарыли их в ядре Objective-C.

Поскольку разработчик Python вдохновлялся «Putting Metaclasses to Work», то там метаклассы есть, но книгу эту до меня никто не сканировал, не выкладывал в сеть, и как этим пользоваться, было не всем понятно.

Что касается собственно RRBC, то и Delphi, и Ada, и C++ все аналогичны C++ в таблице. Generic C++. Ни Delta/C++, ни Sun OBI, ни SOM, ничего из этого не помогло им стать лучше. Generic C++, как в 1995м году. Вам могли нахваливать GObject в Linux? Туда же.

Только Objective-C демонстрирует положительную динамику. С появлением non-fragile ivars (также известное, как non-fragile ABI) была заткнута важная брешь, сноска h в таблице.

Я пытался сделать из Delphi .bpl нечто вроде .NET Framework, чтоб поставил, и остальные приложения по нескольку килобайт были. Это называлось RTL Pak. Очень скоро я не смог угнаться за очередными версиями Delphi, в каждой из которых свои библиотеки RTL и VCL, не подходящие другим версиям Delphi. RTL от GNAT я туда тоже положил, и точно так же он на следующий год устарел.

Для сравнения, вот тут выложены разные версии YellowBox для Windows, а вот тут можно скачать SokoSave для YellowBox, и версия для старого OPENSTEP (когда оно ещё даже не называлось YellowBox) будет работать и с OPENSTEP, и с WebObjects 4.0.1 (в которых YellowBox 1.0), и с YellowBox 5.1. Только с версией для WebObjects получается неприятная история. В WebObjects средства разработки лепят во все исполняемые файлы подряд зависимость от библиотеки, которая может посылать сообщения через machd.exe, а так как machd.exe и эта библиотека есть только в WebObjects, версия для WebObjects не запускается на YellowBox 5.1.

При том, что для Objective-C есть, что показать, серьёзным недостатком получается ублюдочный синтаксис, тяжёлое наследие Smalltalk. Каждый раз, как пытаются сделать работу с API на Objective-C из других языков программирования, начинается паралимпиада, а как бы эти ублюдочные селекторы добавить в свой язык программирования. Может быть, нам эту пакость-таки добавить каким-нибудь макаром в синтаксис, а, может быть, задвинуть весь селектор в отдельную директиву и жить дальше как люди? Естественным путём добровольно никто в свой язык такой способ записи вызовов методов так и не добавил. Кроме того, в Objective-C плоское пространство селекторов, в то время как методы SOM принадлежат классам.

Таким образом, глядя на экосистему Objective-C, которому больше повезло, можно убедиться, что RRBC — это нужно, но в том, чтобы думать о том, как сделать правильно, полезнее смотреть на SOM, которому повезло гораздо меньше.

У меня получилось немного повзаимодействовать с SOM 3.0 из Delphi 7. Учитывая, что SOM двадцать лет не развивался, имеет смысл сбросить балласт наследия и начать с нуля. Так что это скорее для лучшего понимания, чем для реального использования.

Во-первых, в то время, как в COM была изначально диктатура Unicode (2-хбайтовые строки), а Objective-C тоже как-то пережил этот переход, в виде UTF-8 хотя бы, SOM встречает нас опостылевшим ANSI. Однобайтовые строки берут начало в стандарте CORBA. В CORBA потом появился wstring (2-хбайтовые строки), а некоторые применения CORBA считают, что все однобайтовые строки UTF-8. Я работал и с 2-хбайтовыми строками в COM, и с однобайтовыми в ANSI и UTF-8, и с четырёхбайтовыми в Ada. Один человек в 2005м году написал апологетику, как он назвал, Освенциму для однобайтовых строк, и мне с учётом моего опыта работы она понравилась. Если сейчас, 20 лет спустя после Java, которая ему так понравилась, начинать заново, я бы устроил ещё более лютый Освенцим и оставил только четырёхбайтовые строки (UTF-32). Для мира Windows моветон, а взять, допустим, libidn, там, наоборот, нет UTF-16, а есть только UTF-8 и UTF-32.

Во-вторых, очень неудобно, что нет счётчиков ссылок для почти всего, как в COM и Objective-C. IBM SOM 3.0 их так и не приобрёл. У Apple SOM, кстати, они были. В Objective-C они появились не сразу, переход был где-то между NeXTSTEP и OPENSTEP в 1990x, достаточно давно, а теперь-то там вообще ARC. И в Delphi ARC начинает пролезать (на мобильных платформах есть и в версии для Linux обещают) в дополнение к ARC для интерфейсов, которое уже было. Для исключений счетчика ссылок не было, а потом стало можно заворачивать исключения в исключения, и пришлось сделать System.AcquireExceptionObject, ещё один способ подсчёта ссылок. В SOM в отсутствие подсчёта ссылок усложняется управление памятью. Причем, если не предпринять некоторых действий, то управление памятью для обычных объектов и для прокси к удалённым объектам DSOM — разное. Начинать сейчас что-то делать без счётчика ссылок — значит, обрекать себя на дурацкие проблемы потом, когда-таки понадобится иметь нескольких владельцев объекта. Значит, ARC должен быть сразу. Есть мысль, что такое устройство связано с тем, что была попытка сделать такой DirectToSOM компилятор C++, который бы мог более менее любой обычный код на C++ компилировать в SOM. Если в C++ нет счетчика ссылок для объектов, то и в SOM он не добавлялся, зато была куча методов типа somDefaultConstVCopyInit для константных/неконстатных, волатильных/неволатильных конструкторов, деструкторов, операции присвоения. Посмотрев на всё это дело, а также посмотрев на то, как шли дела в C++ with Managed Extensions, C++/CLI, Objective-C++, C++/CX, я понимаю, что всё заканчивается тем, что в языке начинают существовать параллельные иерархии классов, одна из старого C++, другая из соответствующей модели, и все эти заигрывания с «обычным C++» так ничем хорошим нигде не закончились. Зато сейчас есть не только legacy C++ код, но и legacy Objective-C, более многообещающий с точки зрения возможности конвертации.

В-третьих, так как кода для SOM, о котором можно побеспокоиться, почти нет, а кода для Objective-C относительно много, неплохо бы новую модель сделать так, чтобы можно было написать компилятор псевдо-Objective-C, который бы мог перекомпилировать существующий код (вроде GNUStep) под новое ядро. Чтобы это было проще, семантику нового ядра тоже может иметь смысл в некоторых местах подкорректировать подальше от SOM, поближе к Objective-C, но не в ущерб тем возможностям, которые были в SOM. Например, сделать, чтобы вызовы методов у NULL не приводили к ошибке.

В-четвёртых, для разработчиков SOM был важен стандарт CORBA. Можно было скачать SOMobjects Developer Toolkit 3.0, сделать на нём сервер DSOM, потом взять SOM 3.0 Java Client, сделать с его помощью Java-апплет, который по сырым сокетам подключится к серверу и поработает с его объектами. Сейчас это не актуально. Flash вытеснил Java, а теперь уже HTML5 вытесняет Flash. Сырых сокетов в HTML5 нет. Лучше протокол взаимодействия основывать на чём-то, что может работать с WebSockets.

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