Проблема хрупкого бинарного интерфейса или FBI — это недостаток некоторых компиляторов объектно-ориентированных языков программирования , в которых внутренние изменения в базовой библиотеке классов могут привести к прекращению работы дочерних библиотек или программ. Это пример хрупкости программного обеспечения .
Эту проблему чаще называют проблемой хрупкого базового класса или FBC ; однако этот термин имеет более широкий смысл.
Проблема возникает из-за «сокращения», используемого в компиляторах для многих распространенных объектно-ориентированных (ОО) языков, — конструктивной особенности, которая сохранялась, когда ОО-языки развивались из более ранних не-ОО- структурированных языков программирования, таких как C и Pascal .
В этих языках не было объектов в современном смысле, но была похожая конструкция, известная как запись (или «структура» в C), которая содержала разнообразную связанную информацию в одном фрагменте памяти. Доступ к частям внутри конкретной записи осуществлялся путем отслеживания начального местоположения записи и знания смещения от этой начальной точки до рассматриваемой части. Например, запись «person» могла иметь имя, фамилию и отчество, для доступа к инициалу программист пишет thisPerson.middleInitial
, что компилятор преобразует во что-то вроде a = location(thisPerson) + offset(middleInitial)
. Современные процессоры обычно включают инструкции для этого распространенного вида доступа.
Когда впервые разрабатывались компиляторы объектно-ориентированных языков, использовалась большая часть существующей технологии компиляторов, а объекты были построены поверх концепции записи. В этих языках объекты ссылались по их начальной точке, а их общедоступные данные, известные как «поля», были доступны через известное смещение. По сути, единственным изменением было добавление еще одного поля в запись, которое указывает на неизменяемую таблицу виртуальных методов для каждого класса, так что запись описывает как его данные, так и методы (функции). При компиляции смещения используются для доступа как к данным, так и к коду (через таблицу виртуальных методов).
Это приводит к проблеме в более крупных программах , когда они построены из библиотек . Если автор библиотеки меняет размер или расположение публичных полей внутри объекта, смещения становятся недействительными, и программа больше не будет работать. Это проблема ФБР.
Хотя можно ожидать, что изменения в реализации вызовут проблемы, коварство FBI в том, что на самом деле ничего не изменилось, только макет объекта, который скрыт в скомпилированной библиотеке. Можно было бы ожидать, что если изменить это doSomething
, doSomethingElse
то это может вызвать проблему, но в этом случае можно вызвать проблемы без изменения doSomething
, это может быть вызвано так же легко, как перемещение строк исходного кода для ясности. Хуже того, программист имеет мало или вообще не имеет контроля над результирующим макетом, сгенерированным компилятором, что делает эту проблему почти полностью скрытой от глаз.
В сложных объектно-ориентированных программах или библиотеках классы самого высокого уровня могут наследовать от десятков классов. Каждый из этих базовых классов может быть унаследован сотнями других классов. Эти базовые классы хрупкие, потому что небольшое изменение в одном из них может вызвать проблемы для любого класса, который наследует от него, либо напрямую, либо из другого класса, который это делает. Это может привести к тому, что библиотека рухнет как карточный домик , поскольку многие классы будут повреждены одним изменением в базовом классе. Проблема может быть не замечена при написании изменений, если дерево наследования сложное. Действительно, разработчик, изменяющий базовый класс, обычно не знает, какие классы, разработанные другими, его используют.
Одним из решений проблемы хрупкого бинарного интерфейса является написание языка, который знает о существовании проблемы и не позволяет ей возникнуть в первую очередь. Большинство специально написанных ОО-языков, в отличие от тех, которые произошли от более ранних языков, создают все свои таблицы смещений во время загрузки. Изменения в макете библиотеки будут «замечены» в этот момент. Другие ОО-языки, такие как Self , создают все во время выполнения, копируя и изменяя объекты, найденные в библиотеках, и поэтому на самом деле не имеют базового класса, который может быть хрупким. Некоторые языки, такие как Java , имеют обширную документацию о том, какие изменения можно безопасно вносить, не вызывая проблем с ФБР.
Другое решение — записать промежуточный файл, содержащий список смещений и другой информации из этапа компиляции, известной как метаданные. Затем компоновщик использует эту информацию для исправления себя при загрузке библиотеки в приложение. Такие платформы, как .NET, делают это.
Однако рынок выбрал языки программирования, такие как C++ , которые действительно «зависят от позиции» и, следовательно, демонстрируют FBI. В этих случаях все еще есть ряд решений проблемы. Один из них возлагает бремя на автора библиотеки, заставляя его вставлять несколько объектов-«заполнителей» на случай, если в будущем ему понадобится добавить дополнительную функциональность (это можно увидеть в структурах, используемых в библиотеке DirectX ). Это решение хорошо работает, пока у вас не закончатся эти пустышки — и вы не хотите добавлять их слишком много, потому что это занимает память.
Objective-C 2.0 обеспечивает нехрупкие переменные экземпляра , имея дополнительный уровень косвенности для доступа к переменным экземпляра.
Другое частичное решение — использовать шаблон Bridge , иногда называемый « Pimpl » («Указатель на реализацию»). Фреймворк Qt является примером такой реализации. Каждый класс определяет только один член данных, который является указателем на структуру, содержащую данные реализации. Размер самого указателя вряд ли изменится (для данной платформы), поэтому изменение данных реализации не повлияет на размер публичной структуры. Однако это не позволяет избежать других критических изменений, таких как введение виртуальных методов в класс, у которого их нет, или изменение графа наследования.
Другое решение требует более умного компоновщика. В оригинальной версии Objective-C формат библиотеки допускал несколько версий одной библиотеки и включал некоторую функциональность для выбора правильной библиотеки при вызове. Однако это не всегда было нужно, поскольку смещения были нужны только для полей, поскольку смещения методов собирались во время выполнения и не могли вызвать FBI. Поскольку методы, как правило, изменяются чаще, чем поля, ObjC изначально имел мало проблем FBI, а те, которые были, можно было исправить с помощью системы управления версиями. Objective-C 2.0 добавил «современную среду выполнения», которая также решила проблему FBI для полей. Кроме того, язык TOM использует смещения, собранные во время выполнения, для всего, что делает FBI невозможным.
Использование статических библиотек вместо динамических там, где это возможно, является другим решением, поскольку библиотека в таком случае не может быть изменена без перекомпиляции приложения и обновления используемых им смещений. Однако статические библиотеки имеют свои собственные серьезные проблемы, такие как больший двоичный файл и невозможность использовать более новые версии библиотеки «автоматически» по мере их появления.
В этих языках проблема уменьшается за счет принудительного применения одиночного наследования (так как это снижает сложность дерева наследования) и использования интерфейсов вместо базовых классов с виртуальными функциями , поскольку сами интерфейсы не содержат кода, а только гарантию того, что каждая сигнатура метода, объявленная интерфейсом, будет поддерживаться каждым объектом, реализующим интерфейс.
Вся проблема исчезает, если исходный код библиотек доступен. Тогда простая перекомпиляция сделает свое дело.