Введение

Проблема хрупкого бинарного интерфейса (англ. Fragile binary interface problem, FBI) — это недостаток некоторых компиляторов объектно-ориентированных языков программирования, при котором внутренние изменения в базовой библиотеке классов могут привести к неработоспособности производных библиотек или программ. Это пример хрупкости программного обеспечения. Эта проблема чаще называется проблемой хрупкого базового класса или FBC, однако этот термин имеет более широкое значение.

Причина

Проблема возникает из-за "оптимизации", используемой в компиляторах для многих распространенных объектно-ориентированных (ОО) языков – особенности проектирования, сохранившейся при развитии ОО-языков из более ранних не-ОО структурированных языков программирования, таких как C и Pascal. В этих языках не было объектов в современном понимании, но существовала схожая конструкция, известная как запись (или "структура" в C), которая хранила различные связанные данные в одном блоке памяти. Доступ к элементам внутри записи осуществлялся путем отслеживания начального адреса записи и знания смещения от этой точки до нужного элемента. Например, запись "человек" может содержать имя, фамилию и отчество, и для доступа к отчеству программист пишет thisPerson.middleInitial, что компилятор преобразует в нечто вроде a = address(thisPerson) + offset(middleInitial). Современные процессоры обычно имеют инструкции для выполнения подобных операций доступа. При разработке первых компиляторов объектно-ориентированных языков использовались многие существующие технологии компиляции, и объекты строились на основе концепции записи. В этих языках объекты идентифицировались по их начальному адресу, а их публичные данные, известные как "поля", получались через известное смещение. По сути, единственным изменением было добавление еще одного поля в запись, которое указывает на неизменяемую виртуальную таблицу методов для каждого класса, таким образом запись описывает как свои данные, так и методы (функции). При компиляции смещения используются для доступа как к данным, так и к коду (через виртуальную таблицу методов).

Симптомы

Это приводит к проблеме в больших программах, создаваемых из библиотек. Если автор библиотеки изменяет размер или структуру публичных полей объекта, смещения становятся недействительными, и программа перестает работать. Это проблема ФБР. Хотя изменения в реализации могут вызывать проблемы, коварство проблемы ФБР заключается в том, что по сути ничего не меняется, только структура объекта, скрытая в скомпилированной библиотеке. Можно ожидать, что изменение функции `doSomething` на `doSomethingElse` может вызвать проблему, но в данном случае проблемы могут возникнуть и без изменения `doSomething`, например, просто при перестановке строк исходного кода для большей ясности. Хуже того, программист практически не имеет контроля над структурой, генерируемой компилятором, что делает эту проблему почти незаметной. В сложных объектно-ориентированных программах или библиотеках классы верхнего уровня могут наследовать от десятков классов. Каждый из этих базовых классов, в свою очередь, может быть унаследован сотнями других классов. Эти базовые классы уязвимы, поскольку даже небольшое изменение в одном из них может вызвать проблемы для любого класса, который от него наследует, напрямую или через другой класс. Это может привести к тому, что библиотека рухнет как карточный домик, поскольку одно изменение в базовом классе повредит множество классов. Проблема может остаться незамеченной при внесении изменений, особенно если дерево наследования сложное. Фактически, разработчик, изменяющий базовый класс, обычно не знает, какие классы, разработанные другими, его используют.

Языки

Одним из решений проблемы хрупкого бинарного интерфейса является разработка языка, который учитывает эту проблему и предотвращает её возникновение изначально. Большинство специально разработанных объектно-ориентированных языков, в отличие от тех, которые развились из более ранних, строят все свои таблицы смещений во время загрузки. Изменения в структуре библиотеки будут обнаружены в этот момент. Другие объектно-ориентированные языки, такие как Self, конструируют всё во время выполнения, копируя и изменяя объекты, найденные в библиотеках, и, следовательно, фактически не имеют базового класса, подверженного хрупкости. Некоторые языки, такие как Java, содержат обширную документацию о том, какие изменения можно безопасно вносить, не вызывая проблем с бинарной совместимостью. Другим решением является создание промежуточного файла, содержащего смещения и другую информацию, полученную на этапе компиляции, известную как метаданные. Линкер затем использует эту информацию для самокоррекции при загрузке библиотеки в приложение. Платформы, такие как .NET, используют этот подход. Однако рынок выбрал языки программирования, такие как C++, которые действительно "зависят от позиции" и, следовательно, демонстрируют хрупкость бинарного интерфейса. В этих случаях существует ряд других решений. Одно из них возлагает ответственность на автора библиотеки, требуя от него вставки ряда "заглушек" в случае необходимости добавления дополнительной функциональности в будущем (это можно увидеть в структурах, используемых в библиотеке DirectX). Это решение хорошо работает, пока не исчерпаются эти заглушки, и нежелательно добавлять их слишком много, так как это занимает память. Objective-C 2.0 обеспечивает нехрупкие переменные экземпляра, добавляя дополнительный уровень косвенности при доступе к ним. Другим частичным решением является использование шаблона "Мост", иногда известного как "Pimpl" ("Указатель на реализацию"). Фреймворк Qt является примером такой реализации. Каждый класс определяет только один член данных – указатель на структуру, содержащую данные реализации. Размер самого указателя вряд ли изменится (для данной платформы), поэтому изменение данных реализации не влияет на размер публичной структуры. Однако это не предотвращает другие ломающие изменения, такие как добавление виртуальных методов в класс, который их не имеет, или изменение графа наследования.

Сцепляющие

Другое решение требует более интеллектуального линкера. В оригинальной версии Objective-C формат библиотеки допускал наличие нескольких версий одной и той же библиотеки и включал функциональность для выбора нужной версии при вызове. Однако это не всегда было необходимо, поскольку смещения требовались только для полей, а смещения методов собирались во время выполнения и не могли привести к проблеме несовместимости ABI (Application Binary Interface). Поскольку методы меняются чаще, чем поля, в Objective-C изначально возникало немного проблем с несовместимостью ABI, и те, что возникали, можно было исправить с помощью системы версионирования. Objective-C 2.0 представил "современную среду выполнения", которая решила проблему несовместимости ABI и для полей. Кроме того, язык TOM использует смещения, собираемые во время выполнения, для всего, что делает несовместимость ABI невозможной. Использование статических вместо динамических библиотек, где это возможно, является еще одним решением, поскольку библиотека не может быть изменена без перекомпиляции приложения и обновления используемых ею смещений. Однако статические библиотеки имеют и свои серьезные недостатки, такие как больший размер исполняемого файла и невозможность "автоматически" использовать новые версии библиотеки по мере их появления.

Архитектура

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

Способ распределения

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