Насколько понятно самому, что это и зачем нужно, настолько же сложно передать другим. Компонент, который для проекта является ядром, в других системах спрятан в чём-то другом. Я называю его так:
Это лучший способ назвать, который я придумал. А теперь объясняю, откуда такое название. Чтобы на что-то указать, нужно это что-то оттенить другим. Например, если бы все в мире говорили и писали только на русском, было бы очень сложно понять: что такое русский язык? Мы просто говорим. Мы просто пишем. И первая сущность, на которую я хочу указать, — это:
Он есть в почти любой операционной системе. Иногда он в ядре, иногда это по формату исполняемый файл. О том, что его непросто увидеть, свидетельствует расхожее утверждение, что у языка Си нет рантайма. Да нет, вот же он! Отнюдь не по волшебству процедуры из разных динамических модулей находят друг друга.
На входе у нас есть файлы модулей. На входе у нас есть чистое виртуальное адресное пространство процесса. В файлах модулей описаны сегменты кода/данных. И процедурный динамический компоновщик где-то в виртуальном адресном пространстве размещает сегменты и производит релокацию. Затем процедурный динамический редактор связей соединяет модули. Пользуясь знанием о том, где компоновщик разместил другие модули, редактор связей прописывает указатели между модулями. Кроме ссылок на модули, связи идентифицируются именами процедур. Поэтому эти сущности названы процедурными.
Эту сущность можно встретить в трансляторах языков программирования, но только в статическом исполнении. Поэтому трансляторы загораживают собой эту сущность. Разработчик имеет с ней дело, но не воспринимает отдельно от транслятора. И проблема, что реализовано только статически.
Также эту сущность можно встретить в виртуальных машинах: CLI, JVM. На этот раз виртуальные машины загораживают собой эту сущность. Постоянно что-то объемлющее загораживает, поэтому на сущность сложно указать. Проект исходит из предположения, что по соображениям производительности виртуальные машины отклоняются, а раз так, то надо вычленить нужный компонент и рассмотреть его отдельно.
Самое первое, на что нужно указать, — это предмет, с которым работает объектно-ориентированный собрат процедурного. У процедурного компоновщика это виртуальное адресное пространство. А у объектно-ориентированного? А это:
Процедурный компоновщик компонует сегменты динамических модулей в виртуальном адресном пространстве процесса. Объектно-ориентированный компоновщик компонует наборы полей предков класса в адресном пространстве всех будущих объектов класса-потомка. Подобно тому, как сегменты модулей могут расти и сжиматься, сжиматься и расти могут наборы полей базовых классов, реализованных в других динамических модулях. И объектно-ориентированный редактор связей объясняет реализациям унаследованных методов, где найти известные им поля в объектах классов-потомков.
В System Object Model для этого существовала процедура somDataResolve. Получает на вход указатель на объект (this) и класс-предок, даёт на выходе указатель на набор полей, добавленных этим классом (self). Представляется возможным эту схему оптимизироваать.
Задача очень похожая, но на все объекты одного класса таблица общая, так что пространство снова обычное, виртуальное адресное пространство процесса. Каждый класс-предок может привносить то больше, то меньше виртуальных методов. Вслед за ними меняется форма таблиц виртуальных методов, и объектно-ориентированный редактор связей объясняет, как вызвать виртуальный метод.
В System Object Model для этого существовали структуры ClassData и процедура somResolve. Каждый публичный метод, привносимый каким-то классом, получает индекс releaseorder, и по этому индексу в ClassData можно взять жетон метода. somResolve берёт на вход жетон метода и указатель на объект, на выходе даёт ссылку на реализацию метода. При этом в SOM как минимум версии 2.1 была сделана оптимизация: жетоны методов являются исполняемыми, с прототипом, идентичным прототипу метода. Так что не вызываем somResolve. Приводим жетон метода к нужному типу указателя и вызываем, передавая аргументы. А исполняемый жетон дальше разберётся, куда прыгнуть. В машинный кодах это похоже на вызов виртуальных методов COM. И по затратам обычно сопоставимо:
COM | SOM |
---|---|
Запрос адреса интерфейсной VMT по адресу интерефейса | Запрос адреса ClassData по адресу импортированной связи |
Запрос адреса пред-метода по адресу VMT | Запрос адреса исполняемого жетона по адресу ClassData |
Косвенный вызов пред-метода | Косвенный вызов исполняемого жетона |
Пред-метод декрементирует адрес указателя, чтоб переделать адрес интерфейса в адрес объекта | |
Пред-метод запрашивает адрес основной VMT по адресу объекта | Исполняемый жетон запрашивает адрес VMT по адресу объекта |
Пред-метод запрашивает адрес метода по адресу VMT | Исполняемый жетон запрашивает адрес метода по адресу VMT |
Пред-метод косвенно прыгает к реализации метода | Исполняемый жетон косвенно прыгает к реализации метода |
Но эти незначительные отличия в устройстве вызова радикально влияют на гибкость. Ведь в интерфейсной VMT COM некуда добавлять новые методы базовых классов, а ClassData у каждого класса свой, там есть, куда расти.
Пользуясь знанием о том, есть ли у класса потомки, редактор связей может делать исполняемые жетоны, прыгающие сразу в реализацию. Пользуясь знанием о том, всегда ли класс наследуется первым, можно делать исполняемые жетоны, прыгающие по смещению в VMT. Но если это не так, то динамический поиск с кешем, как в Objective-C 2.0. В SOM очень стройно получается множественное наследование, и ещё лучше в PMtW. Удачнее подхода, когда множественное наследование только через интерфейсы.
Также исполняемый жетон мог бы не просто прыгать в метод реализации, а передавать ему вспомогательный список, который бы и свои поля (self) помог найти сходу, и унаследованные реализации помог вызвать, и они тоже по цепочке все могли быстро отработать.
В физике несовпадение импедансов на стыке двух каналов вызывает отражение волн и, тем самым, потерю энергии. В программировании эта аналогия переносится на разный дизайн инструментов. Основная проблема нативных объектно-ориентированных языков программирования имеет сбивающее с толку название проблемы хрупкого базового класса. Но если начать смотреть на динамические компоновщики, то эту проблему можно переформулировать так: у нас трансляторы объектно-ориентированные, а динамический компоновщик до сих пор процедурный. Поэтому и не лезет. Разные потери происходят:
А ведь всё может быть иначе. Процедурный компоновщик и редактор связей предоставлен операционной системой. Если мы поняли, что нам нужен объектно-ориентированный, то в текущих реалиях он будет оформлен как динамическая библиотека. После такого введения должно быть понятно, что это не обычная библиотека и не каркас, а нечто иное, более фундаментальное.
При эволюции библиотек классов растут или сжимаются наборы полей и методов. Похожим образом можно считать «версией 1.0» абстрактный набор классов для GUI, сети и файловой системы, а «версией 1.1» их специализацию под конкретную операционную систему. Таким образом, можно собрать один-два раза (32 и 64 бита) исполняемый или библиотечный модуль, и грузить его потом на разных операционных системах, и он будет вставать как влитой.
Как показано в моей статье, основной бич двоичных трансляторов — косвенные вызовы. Но также там есть и ссылка на проект, в котором эта стоимость испаряется, как только есть, куда положить подсказку двоичному транслятору, где оттранслированный код. Косвенные вызовы через ClassData не только гибки с точки зрения эволюции библиотек классов, но и их можно автоматически распознавать двоичным транслятором. А, распознав, избегать хеш-таблиц. И если все косвенные вызовы делать только через методы объектов, то x86 становится байткодом. Тяжёлая математика и графика делаются родными библиотеками, а логика приложения кодируется x86-байткодом.