4 года назад опубликована моя статья о IBM SOM , где я отметил крайне плачевную ситуацию, когда значимые инструменты были утеряны, и чем дальше, тем меньше шансов на восстановление.
За прошедшее время много чего произошло, найдены и SOM 3.0 для Windows, и SOM 2.1, и открытый клон somFree, и рабочий компилятор DirectToSOM C++ для Windows, и мост в OLE Automation. Один из моих проектов реализует поддержку SOM в Delphi. Разработка началась на Delphi, часть привязок пришлось делать вручную и не так красиво, в процедурном стиле, без проверки типов.
С использованием этих привязок был написан генератор привязок в объектном стиле, а затем сам генератор переписан под новые привязки, подтвердив их работоспособность.
Ради красоты мне пришлось взломать объектную систему Delphi, и возможно вам будет интересно, как это можно сделать.
Некрасивые («тонкие») привязки хоть как-то позволяют взаимодействовать с библиотекой, а красивые («толстые») привязки стремятся сделать это естественным с точки зрения обычного кода.
IBM SOM (системная объектная модель) — это объекты, а у объектов есть методы, вызываемые через точку.
Мне известны следующие объекты в Delphi, методы которых могут вызываться в объектном стиле:
- Объекты
- Объекты из старой объектной системы Borland Pascal с объектами
- Классы
- Интерфейсы
- Дисинтерфейсы
- Параметры
- Начиная с Delphi 2006, записи
- Начиная с Delphi XE3, все, для чего виден помощник
Что касается разработки привязок, то начало поддержки SOM с помощью Delphi, а не Ada, связано с тем, что в Delphi это сделать сложнее.
А внутри Delphi самое сложное — создать блок типов, и именно здесь началась разработка инструмента импорта.
Победить тип – значит решить основные проблемы.
В принципе, хорошо, что в Delphi есть модульность.
Это лучше, чем неудобства C++ или, особенно C, которые возникают при использовании эмиттеров SOM DTK. А там, где нет привычного раздражения, добавляются тонны макросов.
Но в то же время в языке Ada, аналогичном Delphi, появились «private with», «type X;», «limited with», позволяющие связывать ограниченную информацию о типах и тем самым иметь как модульность, так и свободу создавать циклические соединения между модулями и тем более делать циклы внутри одного пакета, но в Delphi развитие в этом направлении продвигалось слабо.
Еще хуже то, что основной компилятор коммерческий (в лучшем случае один заказ на FPC против 6 на Delphi, а Аду приходится пихать, не попадается, сходить куда-нибудь ради Ады Я тоже не хочу), поэтому они используют старые версии, Delphi 7 по-прежнему актуален, поэтому если я хочу написать библиотеку на любимой Аде и выставить ее так, чтобы ее можно было использовать из Delphi, то желательно бы, чтобы это может быть Delphi 7, поэтому последние 2 варианта отметаются.
Опции не имеют контроля типа, список методов не всплывает во всплывающей подсказке, эта опция также отбрасывается.
Изначально я хотел сделать RAII, чтобы память управлялась автоматически, как строки и интерфейсы, а для этого нужно обернуть интерфейс или параметр в запись Delphi 2006 или старый объект .
У этого подхода есть существенный недостаток.
Согласно правилам Delphi, вперед можно объявлять только классы, интерфейсы и указатели, причем в неделимом блоке типов.
А теперь, допустим, мы делаем привязки к классу Container и классу Contained, и они связаны между собой.
Было бы неплохо написать Container = Record Private FSomething: Variant; конец; и аналогично для Contained, а потом уточнить, какие у них методы, ведь методы могут принимать ссылки на SOM-классы, проецируемые в записи, а если методы можно писать только внутри записи, то одна из записей явно еще не будет объявлена.
Это означает, что вся запись не может быть объявлена из-за аргументов метода.
Частично ситуацию можно спасти, если сначала сделать для каждого класса СОМ-объект из старой системы объектов со скрытым полем, но без методов, а потом снова наследовать, и в каждом методе брать на вход ненаследуемый вариант без методов, чтобы унаследованные из которых будут автоматически преобразованы методами.
Но будет проблема с результатом; наоборот, из-за ограничений Delphi метод класса SOM, привязки для которого созданы раньше другого, не сможет в результате вернуть унаследованную версию, обросшую методами возвращаемого класса SOM. Container и Contained находятся в несколько более сложных отношениях; они возвращают не друг друга, а последовательность (аналог динамического массива Корба) друг от друга, а значит, последовательность нужно будет проецировать конкретно для объектов, которые не наследуются и не обрастают методами.
И только в Delphi XE3 появился чуть менее громоздкий способ разделения объявления структуры и методов.
Сначала мы оборачиваем интерфейс или опцию в приватную часть записи и так далее для каждого проецируемого класса, а затем присоединяем методы с помощью хелперов.
И эти методы-помощники уже могут спокойно принимать на вход и выход все, что нужно.
Разобравшись с управлением памятью в SOM, мне больше не хотелось заниматься RAII. Дело в том, что в IBM-версии SOM нет счетчика ссылок для всех объектов, как есть в COM и современном Objective-C, и что делать, если ссылка на объект скопирована? Кстати, у Apple SOM был , а оттуда пошло в somFree, так что не безнадежно, но в то же время в моем распоряжении есть компилятор DirectToSOM C++ и мост к OLE Automation, с которым мне бы хотелось, чтобы мое решение было совместимо на данный момент, и они не рассчитаны на такой режим работы.
С интерфейсами и обычными классами-обертками возникают проблемы типа «что делать, если скопирована ссылка на объект», только в случае уничтожения.
В конце концов, обертка может транзитивно уничтожить объект SOM, а может и нет. Как минимум, оболочки должны иметь флаг владения.
А для полного счастья еще и запутаться во всем этом.
Если бы у нас был счётчик ссылок, мы бы всегда его дергали и не размышляли и не путались.
Все будет работать как часы.
Мы бы жили хорошо.
Если трогать старую объектную систему, то начинают сыпаться предупреждения.
Вот так я и пришел к решению взломать объектную систему Delphi. Мой генератор проецирует классы SOM в классы Delphi с помощью обычных методов, и все это используется привычным для Delphi-разработчика способом.
Отложенные объявления отлично подходят для классов; вам не нужно их ни во что потом оборачивать.
Поскольку все циклы необходимо замыкать в одном блоке типов, все модули CORBA и все типы, вложенные в классы, приходится проецировать в один модуль Delphi, чтобы этот блок имел один блок типов.
Началось с того, что я решил заставить Delphi считать объекты SOM объектами Delphi. Пока Delphi не трогает VMT, все в порядке.
Каждый метод класса SOM соответствует методу класса Delphi. При этом методы СОМ, как правило, являются виртуальными и проецируются на невиртуальные методы Delphi, умеющие отправлять вызов СОМ.
Невиртуальные методы вызываются без вмешательства в VMT, и Delphi не знает, что там обрабатываются не-Delphi-объекты.
Методы в SOM легко вызывать.
Здесь вы видите 8 инструкций (домашнее задание — попытаться понять, что они делают), из которых двух достаточно для собственно вызова.
Перед последним вызовом дважды выполняется mov/push, это передача аргументов, а не вызов.
Перед этим не обязательно прописывать адрес в var_14 и потом вызывать его; вы можете написать адрес в edx в первой инструкции и в конце сделать вызов с помощью [edx + 1Ch].
Еще минус 2, всего 2 инструкции по вызову метода объекта SOM, аналогично 2 инструкциям по вызову виртуальных методов из VMT в других системах разработки.
По полученному адресу находится динамически создаваемый кусок кода, который знает, как лучше вызвать указанный метод, и он будет давать дополнительные «скрытые» инструкции, но какая разница! Об этой разнице вы можете прочитать в перевод отчета «Межрелизная бинарная совместимость» .
Если вы когда-нибудь хотели понять, почему каждая версия Delphi имеет свои собственные наборы dcu и bpl, теперь вы это знаете.
Вернемся к генерации привязок.
Наследование в SOM множественное, и оно активно используется.
Например, при разработке генератора я работаю с OperationDef (метаинформацией о методе), и она одновременно Содержится внутри класса и Контейнера его аргументов.
А в классах Delphi - одиночные.
Теоретически в проекции Delphi можно делать одиночное наследование для первых родителей, а методы не первых родителей добавлять позже, как если бы они появились заново.
Мне это показалось некрасивым, асимметричным решением.
Ведь OperationDef (как и ModuleDef, InterfaceDef) в равной степени являются Container и Contained, и я даже не сразу помню, в каком порядке объявлены их родительские классы.
Плюс, если позволить иерархии классов Delphi разрастаться, то возникают другие проблемы, подробнее об этом в абзаце после следующего.
Поэтому я спроектировал классы SOM так, чтобы они не были родителями друг друга в Delphi. Все они происходят из служебного класса SOMObjectBase, который нужен для сокрытия методов TObject, и методы в них каждый раз заполняются с нуля.
Операции «как» и «есть», конечно, не поддерживаются, потому что они возьмут VMT объекта SOM и подумают, что это VMT объекта Delphi. К сожалению, заблокировать их не получается, но для того, чтобы все было хоть как-то типизировано, для каждого родительского класса генерируются функции As_ ИмяКласса для повышающего приведения и функции класса Supports для понижающего приведения.
Внимательных читателей должно было смутить словосочетание «функция класса», ведь раньше было написано «пока Delphi не трогает VMT, всё в порядке».
Вы спрятали методы класса TObject и открыли свои собственные? Ведь для обычного объекта можно выбрать Supports из выпадающего списка, и Delphi перейдет в VMT. Оказывается, с этим можно справиться элегантно.
Новые методы класса также не являются виртуальными, и все, что Delphi делает для их вызова на объекте, — это берет указатель на нулевое смещение объекта, и это становится Self в методе класса.
А если вы вызываете метод класса, обращаясь к нему по имени, то Self — это VMT Delphi класса.
А как отличить класс VMT SOM от класса VMT Delphi? Но есть один способ.
Все методы класса каждый раз добавляются заново в следующий класс Delphi, от которого обычно никто не наследуется с помощью Delphi, а сами проектируемые классы не наследуются друг от друга с помощью Delphi. Таким образом, у нас может быть только один возможный VMT класса Delphi в Self — это VMT того самого класса Delphi, иначе метод вызывался на объекте SOM, а Self на самом деле является VMT SOM. Устройство SOM VMT неизвестно, оно зависит от версии SOM.DLL и может меняться, но известно, что оно содержит ссылку на класс объекта по нулевому смещению, а это то, что нам нужно.
В SOM все классы также являются объектами, а методы классов — методами объектных классов в самом обычном смысле.
Таким образом, сравнив Self со своим именем, вы затем можете выбрать либо получить ссылку на класс SOM по нулевому смещению в структуре ClassData, либо, если она не совпадает, взять ссылку на класс SOM, разыменовав Self. А затем сделайте то, что подразумевает метод класса.
Собственно, есть два метода класса, которые производят такое сравнение, это ClassObject и NewClass, второй отличается тем, что если в структуре ClassData ничего нет, то класс автоматически не создается.
Остальные методы класса вызывают один из этих двух.
Например, если нам нужно проверить, является ли такой-то объект наследником такого-то класса, то если класс не был создан, то это не обязательно, и ясно, что это не так, но если просят InstanceSize, то без создания класса не обойтись.
Таким образом, и «o.InstanceSize» (для «var o: SOMObject»), и «SOMObject.InstanceSize» будут работать корректно.
Все как в Делфи.
Была идея спроецировать все методы объекта класса SOM в методы класса Delphi, но здесь возникли трудности.
Преодолели, но от их преодоления было решено отказаться.
Трудности при проектировании методов класса SOM в методы класса Delphi. Во-первых, в Delphi классы не создаются динамически, а в SOM они создаются, и все это сопровождается вызовами методов, которые метакласс может переопределить, чтобы выполнить какое-нибудь хитрое поведение.
Например, так называемые кооперативные метаклассы могут вставлять свою собственную реализацию перед определенным методом, который всегда будет вызываться первым, независимо от того, как классы наследуются, а затем передавать управление обычной реализации способом, аналогичным вызову родительского метода.
Метаклассы «До/После» могут добавлять перед всеми методами, независимо от сигнатуры, некий общий код, который будет работать до обычного метода, а на обратном пути — после.
Например, вход и выход из мьютекса.
Или распечатайте события входа и выхода метода на консоль.
Прокси для удаленных вызовов процедур тоже непростая задача.
И сбрасывать все методы, делающие это возможным, в методы класса Delphi, казалось некрасивым решением.
Во-вторых, к классам из СОМ ДТК делаются привязки, а они, как правило, скрывают свои метаклассы, а во всем ДТК есть только одиночный метакласс, который публично виден.
В-третьих, SOM гарантирует (вплоть до искусственного скрещивания), что метакласс любого потомка будет потомком метакласса, и проблемы несовместимости метакласса не возникнет, а текст привязок, который бы это красиво описывал, будет очень избыточно.
Как оказалось, мы можем даже исправить тип результата somGetClass, только если метакласс явно указан.
Если какой-то гипотетический компилятор типизированного языка программирования, поддерживающий SOM или аналогичную модель, видит, что существует переменная, содержащая некоторый дочерний элемент класса X с родителями Y и Z, и Y имеет явный метакласс MY, а Z имеет MZ, а X имеет метакласс MX упорядочен, но на самом деле будет минимальный потомок MY, MZ и MX, тогда типизированный компилятор может взломать тип результата somGetClass так, чтобы он был «MY&MZ&MX» со всеми методами, которые у них есть, и если вызвать somGetClass этого class, чтобы объединение продолжало собираться, но при создании привязок генерировать каждое такое потенциальное объединение было бы слишком много.
Текст уже продублирован для поддержки множественного наследования.
Это означает, что среди классов SOM DTK, для которых известен метакласс, остаются только те, которые сами это сделали явно, но их потомки больше не существуют, если только они не повторяют указание явного метакласса.
В общем, вам нужно написать «o.ClassObject. КлассОбъектМетод ", ну и для некоторых методов класса, которые были в TObject, всё же был сделан удобный доступ.
Вдохновленный тем, как работает TLIBIMP.exe, я сделал его функцией класса.
Получается, как и в Delphi, пишем «repo := Repository.Create;».
Но тут возникла идея, а что если конструкторы SOM (инициализаторы в терминологии SOM) сделать конструкторами с точки зрения Delphi. Чтобы при вызове класса они создавали объект, а на объекте работали как методы.
Чтобы показать, как здесь можно взломать классы Delphi, я решил привести временную диаграмму того, как обычно создаются и уничтожаются объекты в Delphi:
Outer-Create и Outer-Destroy — это код, в который автоматически оборачиваются вызовы конструктора и деструктора.Outer-Create Outer-Create => virtual NewInstance Outer-Create => virtual NewInstance => _GetMem Outer-Create => virtual NewInstance Outer-Create => virtual NewInstance => non-virtual InitInstance Outer-Create => virtual NewInstance => non-virtual InitInstance => FillChar(0) Outer-Create => virtual NewInstance => non-virtual InitInstance Outer-Create => virtual NewInstance Outer-Create Outer-Create => Create Outer-Create Outer-Create => virtual AfterConstruction Outer-Create Free Free => Outer-Destroy Free => Outer-Destroy => virtual BeforeDestruction Free => Outer-Destroy Free => Outer-Destroy => Destroy Free => Outer-Destroy Free => Outer-Destroy => virtual FreeInstance Free => Outer-Destroy => virtual FreeInstance => non-virtual CleanupInstance Free => Outer-Destroy => virtual FreeInstance => non-virtual CleanupInstance => _FinalizeRecord Free => Outer-Destroy => virtual FreeInstance => non-virtual CleanupInstance Free => Outer-Destroy => virtual FreeInstance Free => Outer-Destroy => virtual FreeInstance => _FreeMem Free => Outer-Destroy => virtual FreeInstance Free => Outer-Destroy Free
Что касается SOM, то если вам нужно вызвать нестандартный конструктор (не somInit), то на объекте класса вместо somNew вызывается функция somNewNoInit, возвращающая объект, а затем на нем вызывается тот конструктор, который нужен, для например, somDefaultCopyInit. Или тот же somInit. Идея состоит в том, чтобы каким-то образом взломать все методы TObject, чтобы последовательность создания объектов воссоздавалась в реалиях Delphi. В частности, мы видим, что TObject.NewInstance — это функция виртуального класса.
Его не обманешь трюками с именами; компилятор Delphi вызывает его из VMT по определенному адресу.
Но вы можете не только скрыть NewInstance в SOMObjectBase, где скрыты методы TObject, но и предоставить осмысленную реализацию, которая будет вызывать somNewNoInit в соответствующем классе SOM. Где она возьмет этот класс? Например, вы можете использовать Delphi VMT для расширения функции защищенного виртуального класса, которая сможет возвращать соответствующий себе класс SOM. Есть только одна проблема.
В конце Outer-Create вызывается виртуальный метод AfterConstruction. Это не сработает, если у объекта уже есть SOM VMT. Можно, конечно, в конце Create временно переписать VMT объекта с SOM на Delphi, а в AfterConstruction — обратно, но это какая-то слишком кислотная схема.
Поэтому в этом вопросе нам пришлось отступить.
Но в остальном привязки оказались вполне естественными.
Наследование от Delphi не реализовано, но если оно и будет, то красиво там сделать будет несколько сложно.
Даже если рассматривать обычные эмиттеры для C++, то их работа с объектами SOM аналогична работе с объектами C++, оператор new() и оператор new(void*) перегружаются, но при наследовании реализация методов классов SOM не происходит. вообще похоже на реализацию методов класса C++.
Кроме специально модифицированного компилятора DirectToSOM C++, конечно.
Данная деятельность осуществляется в рамках изобретательского проекта и в настоящее время носит исследовательский и демонстрационный характер.
Мне нужно изучить подводные камни от А до Я, другим нужно показать фундаментальную осуществимость.
Может быть, где-то оно и пригодится, но окончательный план — работать с другой, новой моделью, которая вберет в себя лучшие возможности SOM, COM и Objective-C и будет готова работать над текущими, которых не было до предыдущие авторы SOM задания .
Теги: #ibm #delphi #SoM #OOP #metaclass #metaclass #bindings #x86 asm #программная инженерия #Assembler #delphi #Системное программирование #ООП
-
Восстановление Данных С Ноутбуков
19 Oct, 24 -
Как Сэкономить На Видеоиграх
19 Oct, 24 -
Лидер С Двумя Головами
19 Oct, 24 -
Тестовая Съемка Серверных Корпусов
19 Oct, 24 -
Как Оценить Преимущества Перехода В Облако
19 Oct, 24 -
Идея. Литературные Путешествия
19 Oct, 24