Андрей Боровский
kylixportal@narod.ruВозможно ли это? До недавнего времени я считал, что обращаться к классам C++, экспортируемым разделяемыми библиотеками Linux, из модулей Kylix можно только при помощи специальных библиотек, написанных на C++ и экспортирующих функции-оболочки для методов классов. В этой статье я расскажу, в основном на основе собственных изысканий, как обращаться к классам C++ непосредственно, без помощи промежуточных библиотек.
В своем заблуждении относительно невозможности непосредственного доступа к классам C++ из Kylix я был не одинок. Например, в книге "Переход на Kylix для Delphi-программистов", написанной Доном Тейлором и соавторами и выпущенной недавно в русском переводе издательством "Питер", говорится (стр. 80, раздел "Функции-члены"): "В Си++ функции-члены (функции, связанные с классом) должны передать адрес объекта, для которого они вызываются, в качестве первого параметра функции. Обычно компилятор Си++ выполняет это автоматически и явно. Однако вы не используете его и не знаете (и не можете узнать) адрес объекта Си++. До тех пор, пока компания Borland не выпустит объединенный пакет Kylix и Borland С++ ... функции-члены Си++ будут недоступны для программистов Kylix."
Я не совсем понимаю, что значит "узнать" адрес объекта C++, но вот создать объект C++ (и, естественно, получить указатель на него) в Kylix можно! В конце концов, экземпляр класса C++ - это просто блок памяти, содержащий все данные этого конкретного экземпляра. Методы (функции-члены) класса - это обычные функции, экспортируемые библиотекой, в которой хранится класс. Вызывая функцию-член для экземпляра класса, мы должны (как справедливо отмечено в процитированном выше отрывке) передать ей в качестве первого параметра указатель на область данных этого экземпляра.
Итак, сейчас мы сделаем то, что авторы указанной книги, да и многие другие программисты считают невозможным. Для того чтобы создать экземпляр класса C++ в Kylix, необходимо выполнить следующие действия: выделить блок памяти в глобальной области (процедурой GetMem), при этом мы, естественно, получим указатель на этот блок, а затем вызвать конструктор класса, передав ему полученный указатель.
Но как же вызвать конструктор? Очень просто. Конструктор - это одна из функций, экспортируемых библиотекой, содержащей класс. Правда, нас подстерегает еще одна проблема. Для того чтобы выделить блок памяти, соответствующий экземпляру класса, необходимо знать размер области данных класса. За этой информацией нам придется обратиться к C++, тут уж ничего не поделаешь. Напишем короткую программу на C++, в которой объявим переменную соответствующего класса и затем с помощью sizeof определим размер этой переменной.
Проиллюстрирую все вышесказанное на простом примере. Пусть у нас есть класс
C++:class MyClass { public: MyClass(int va, int vb); int getSum(); int getDiff(); private: int a, b; }; |
Поместите этот текст в файл myclass.h и создайте файл myclass.cpp следующего содержания:
#include "myclass.h" MyClass::MyClass(int va, int vb) { a = va; b = vb; }; int MyClass::getSum() { return a + b; }; int MyClass::getDiff() { return a - b; }; |
Как видите, у нашего класса есть конструктор и две функции-члена. Кроме того, класс содержит две переменные типа int.
Скомпилируйте библиотеку:
gcc -c myclass.cpp
ld myclass.o -shared -fPIC -soname libmylib.so -o libmylib.so.1
У вас появится файл разделяемой библиотеки libmylib.so.1, содержащей класс MyClass. Скопируйте этот файл в каталог kylix/bin/ и дайте в этом каталоге команду:
ln -sf libmylib.so.1 libmylib.so
Теперь можно посмотреть, какие функции экспортирует библиотека:
nm libmylib.so | grep " T "
При выполнении этой команды на экран выводится следующее:
000002f0 T __7MyClassii
00000330 T getDiff__7MyClass
00000310 T getSum__7MyClass
Третий столбец - имена экспортируемых функций. __7MyClassii - это функция-конструктор класса, другие две функции экспортируют методы getDiff и getSum соответственно.
Далее следует определить размер экземпляра класса. Для этого напишем простейшую программу на C++:
#include "myclass.h" void main() { MyClass MC(1, 2); printf("Size of MyClass = %i\n", sizeof(MC)); } |
Назовите программу
getsize.cpp, скомпилируйте:gcc getsize.cpp myclass.cpp -o getsize
и запустите:
./getsize
Программа выдаст результат:
Size of MyClass = 8
Как видите, размер экземпляра класса равен 8 байтам, т.е. суммарному объему памяти, занимаемому переменными, объявленными в классе. Не следует, однако, обобщать этот результат. Класс MyClass не является потомком какого-либо другого класса. Для большинства же классов, являющихся производными других классов, объем памяти, занимаемый экземпляром класса, нельзя определить, просто суммируя память, занимаемую переменными.
Теперь у нас есть все необходимые сведения о классе MyClass и мы можем приступить к написанию интерфейсного модуля для Kylix. Ниже приводится текст этого модуля:
unit MyClassIntf; interface uses SysUtils; type MyClassP = class(TObject) end; function MyClass_create(a, b : Integer) : MyClassP; procedure MyClass_destroy (P : MyClassP); function MyClass_getSum(P : MyClassP) : Integer; cdecl; function MyClass_getDiff(P : MyClassP) : Integer; cdecl; implementation const libname = 'libmylib.so'; SizeOfMyClass = 8; function _MyClass(P : MyClassP; a, b : Integer) : Pointer; cdecl external libname name '__7MyClassii'; function MyClass_getSum(P : MyClassP) : Integer; cdecl external libname name 'getSum__7MyClass'; function MyClass_getDiff(P : MyClassP) : Integer; cdecl external libname name 'getDiff__7MyClass'; function MyClass_create(a, b : Integer) : MyClassP; begin GetMem(Pointer(Result), SizeOfMyClass); _MyClass(Result, a, b); end; procedure MyClass_destroy(P : MyClassP); begin FreeMem(Pointer(P)); end; end. |
Хотя переменная типа MyClassP используется как простой указатель на блок памяти, мы определяем тип MyClassP как класс. Зачем это нужно, станет ясно позже. Функция MyClass_create создает экземпляр класса C++. Сначала функция выделяет блок памяти соответствующего размера, а затем вызывает функцию _MyClass, импортирующую функцию-конструктор из библиотеки. Обратите внимание на параметры функции _MyClass. У конструктора класса MyClass только два явных параметра, оба типа int, однако первым параметром, фактически передаваемым функции-конструктору, является указатель на область памяти экземпляра класса. Этот же указатель должен быть первым параметром при вызове функций-методов.
Процедура DestroyMyClass уничтожает экземпляр класса. Если бы у класса MyClass была функция-деструктор, ее следовало бы вызвать вместо процедуры FreeMem.
Итак, у нас есть модуль Kylix, позволяющий работать с классом C++ без всяких промежуточных оболочек. Однако в реальной жизни все обстоит несколько сложнее. Первая проблема связана с именами экспортируемых функций. Для импорта методов нам необходимо знать имена соответствующих функций во внутреннем формате C++ (mangled names). Если разделяемая библиотека, содержащая класс, экспортирует таблицу символов (как это было в нашем случае), узнать имена всех функций можно при помощи команды nm. Однако многие библиотеки Linux не экспортируют таблицы символов. Как же узнать имена экспортируемых функций в этом случае? Прежде всего, можно заметить, что модификация имен функций C++ производится по определенной схеме. Для методов классов C++ имя метода, экспортируемого библиотекой, в общем случае выглядит так: ИмяМетода__СписокПараметров.
Здесь ИмяМетода соответствует имени метода в объявлении класса, а список параметров закодирован специальными символами. Например, параметру типа int соответствует символ i, параметру типа char * соответствует сочетание Pc, указатель на экземпляр класса кодируется несколькими символами, включающими имя класса. Более подробную информацию об изменении имен C++ компилятором gcc можно найти в широко распространенном в Сети документе "G++ Internals". Другое решение проблемы - поиск имен экспортируемых функций в теле самой библиотеки при помощи шестнадцатеричного редактора. Перечень имен функций, разделенных символами #0, следует за ключевым словом _DYNAMIC_OFFSET_TABLE_. Получив этот список, нетрудно сопоставить имена экспортируемых функций с именами методов класса C++.
Другая проблема связана с наследованием. Очень часто класс, содержащийся в одной из библиотек, является потомком класса, содержащегося в другой библиотеке. В этом случае все наследуемые методы должны импортироваться из той библиотеки, в которой они объявлены. Допустим, у нас есть два класса: класс Ancestor, содержащийся в библиотеке libanc.so, и класс Descendant, являющийся потомком Ancestor и хранящийся в библиотеке libdesc.so. Класс Ancestor обладает публичным методом AncMeth, который наследуется классом Descendant. Как реализовать Kylix-интерфейс для этих классов? Прежде всего, создадим иерархию указателей, объявляя их как классы:
type AncestorP = class(TObject) end; DescendantP = class(AncestorP) end;
Следует особо подчеркнуть, что переменные типа AncestorP и DescendantP не являются "настоящими" объектами Object Pascal. В частности, операции is и as не будут выполняться корректно с переменными этих типов.
Теперь приступаем к импортированию функций-членов класса. Метод AncMeth импортируется из библиотеки libanc.so:
procedure Ancestor_AncMeth(P : AncestorP; ...); cdecl external 'libanc.so' ...
Для того чтобы вызвать метод AncMeth для экземпляра класса Descendant, необходимо либо вызвать процедуру Ancestor_AncMeth, передав ей в качестве первого параметра переменную типа DescendantP (благо иерархия ссылок это позволяет), либо объявить в модуле процедуру
procedure Descendant_AncMeth(P : DescendantP; ...); cdecl external 'libanc.so' ...
импортирующую ту же функцию-член из библиотеки libancs.so. В любом случае, чтобы сделать все методы класса
Descendant доступными в Kylix, следует проследить всю иерархию этого класса, импортируя методы из соответствующих библиотек.Сходная ситуация возникает при импортировании конструкторов и деструкторов. Если у класса Descendant нет явных конструкторов или деструкторов, это не значит, что их нет и у класса Ancestor. Определяя функцию Descendant_create, следует найти в иерархии Descendant ближайшего предка с явно объявленным конструктором, и вызвать соответствующую функцию.
Так же следует поступать и с деструкторами. Отмечу, что конструктор и деструктор класса, сгенерированные в C++, автоматически вызывают соответственно конструкторы и деструкторы предков данного класса в последовательности, соответствующей иерархии классов. Кроме того, деструктор автоматически освобождает память, выделенную для класса (даже если этот деструктор объявлен в одном из классов-предков). Так что если в процедуре Descendant_destroy вызывается деструктор класса Ancestor, вызывать процедуру FreeMem не нужно.
Рассмотрим еще один пример. Пусть у нас есть два класса, Ancestor и Descendant, определенные следующим образом:
Файл ancs.h:
#ifndef _ANCS #define _ANCS #include <stdlib.h> #include <iostream.h> #include <string.h> class Ancestor { public: Ancestor(const char * vstr); ~Ancestor(); void printStr(); virtual void strOut(); protected: char * str; }; #endif |
Файл ancs.cpp:
#include "ancs.h" Ancestor::Ancestor(const char * vstr) { str = strdup(vstr); } Ancestor::~Ancestor() { free(str); } void Ancestor::printStr() { strOut(); } void Ancestor::strOut() { cout << str; } |
файл desc.h:
#ifndef _DESC #define _DESC #include "ancs.h" #include <qlabel.h> class Descendant: public Ancestor { public: Descendant (const char * vstr, QLabel * vLabel); void strOut(); protected: QLabel * Label; }; #endif |
Файл desc.cpp:
#include "desc.h" Descendant::Descendant (const char * vstr, QLabel * vLabel):Ancestor(vstr) { Label = vLabel; } void Descendant::strOut() { Label->setText(str); } |
Библиотеки libancs и libdesc компилируются следующим образом:
gcc -c ancs.cpp
ld ancs.o -shared -fPIC -soname libancs.so -o libancs.so.1
gcc -c desc.cpp -I/usr/include/qt2
ld desc.o -shared -fPIC -soname libdesc.so -o libdesc.so.1 -lancs -lqt
Kylix-модуль, импортирующий методы объектов, будет выглядеть так:
unit ClassIntf; interface uses SysUtils, Qt; type AncestorP = class(TObject) end; DescendantP = class(AncestorP) end; function Ancestor_create(const Str : PChar) : AncestorP; procedure Ancestor_destroy(P : AncestorP); procedure Ancestor_printStr(P : AncestorP); cdecl; procedure Ancestor_strOut(P : AncestorP); cdecl; function Descendant_create(const Str : PChar; QLabel : LabelH) : DescendantP; procedure Descendant_destroy(P : DescendantP); procedure Descendant_strOut(P : DescendantP); cdecl; procedure Ancestor_strOutV(P : AncestorP); implementation const libancs = 'libancs.so'; libdesc = 'libdesc.so'; Ancestor_Size = 8; Descendant_Size = 12; function _Ancestor(P : AncestorP; Str : PChar ): AncestorP; cdecl external libancs name '__8AncestorPCc'; procedure __Ancestor(P : AncestorP); cdecl external libancs name '_._8Ancestor'; procedure Ancestor_printStr(P : AncestorP); cdecl external libancs name 'printStr__8Ancestor'; procedure Ancestor_strOut(P : AncestorP); cdecl external libancs name 'strOut__8Ancestor'; function _Descendant(P : DescendantP; Str : PChar; QLabel : QLabelH): DescendantP; cdecl external libdesc name '__10DescendantPCcP6QLabel'; procedure __Descendant(P : DescendantP); cdecl external libdesc name '_._10Descendant'; procedure Descendant_strOut(P : DescendantP); cdecl external libdesc name 'strOut__10Descendant'; function Ancestor_create(const Str : PChar) : AncestorP; begin GetMem(Pointer(Result), Ancestor_Size); _Ancestor(Result, Str); end; procedure Ancestor_destroy(P : AncestorP); begin __Ancestor(P); end; function Descendant_create(const Str : PChar; QLabel : QLabelH) : DescendantP; begin GetMem(Pointer(Result), Descendant_Size); _Descendant(Result, Str, QLabel); end; procedure Descendant_destroy(P : DescendantP); begin __Ancestor(P); end; procedure Ancestor_strOutV(P : AncestorP); type TStrOut = procedure(P : AncestorP); cdecl; var StrOut : TStrOut; PP : ^Pointer; begin PP := Pointer(P); Inc(PP, 1); PP := PP^; Inc(PP, 2); StrOut := PP^; StrOut(P); end; end. |
Большая часть этого листинга должна быть вам понятна. Вопросы может вызвать только функция Ancestor_strOutV. Обратите внимание на листинг ancs.h. Метод strOut объявлен как виртуальный. Это значит, что при вызове процедуры Ancestor_printStr для объекта Descendant строка будет передана объекту QLabel, а не распечатана в стандартный поток вывода (т.е. Ancestor_printStr вызовет процедуру, соответствующую Descendant_strOut).
В принципе, с виртуальными методами можно работать так же, как и с любыми другими функциями-членами классов. В модуле ClassIntf мы импортируем виртуальные функции-члены из библиотек libancs и libdesc, и эти функции можно вызвать явным образом для экземпляров соответствующих классов. Однако нам хотелось бы использовать в нашем Kylix-коде преимущества виртуальных методов. Речь идет о ситуации, эквивалентной следующему фрагменту кода C++:
Ancestor * object = new Descendant(...); object->strOut();
Хотя переменная object объявлена как указатель на класс Ancestor, во второй строке будет вызван метод класса Descendant. Существует несколько способов реализации подобного полиморфизма при импорте объектов С++ в Kylix. Один из возможных вариантов заключается в том, чтобы отслеживать тип каждой полученной ссылки и всегда вызывать соответствующий метод, импортированный явно.
Мы же пойдем другим путем, задействовав "настоящую" виртуализацию. Рассмотрим процедуру Ancestor_strOutV. Эта процедура - "виртуальный" аналог процедуры Ancestor_strOut, т.е. при передаче в Ancestor_strOutV указателя на экземпляр какого-либо потомка класса Ancestor будет вызван метод strOut класса-потомка.
Для того чтобы понять, как работает процедура Ancestor_strOutV, нам придется еще больше углубиться во внутренности gcc. Обратите внимание на то, что размер экземпляра класса Ancestor равен 8, а не 4 байтам, как можно было бы предположить. Дополнительные 4 байта - это указатель на таблицу виртуальных методов (vtable) класса Ancestor. Следующие диаграммы иллюстрируют распределение памяти экземпляров Ancestor и Descendant:
Как видите, класс Descendant наследует указатель на таблицу виртуальных методов класса Ancestor. У каждого класса-потомка класса Ancestor этот указатель будет содержать адрес "своей" таблицы виртуальных методов, которая, в свою очередь, будет содержать адрес метода strOut для данного класса.
В процедуре Ancestor_strOutV мы сначала получаем указатель на таблицу виртуальных методов, соответствующую данному экземпляру, а затем извлекаем из этой таблицы адрес метода strOut. Первые четыре байта таблицы в нашем случае равны нулю. Далее следует указатель на функцию type_info, используемую gcc. Третий указатель в таблице содержит адрес метода strOut.
Использование объектов C++ в Kylix нельзя назвать простым делом. Для этого придется не только исследовать иерархию предков импортируемого класса, но и разобраться в таких деталях его реализации, как таблицы виртуальных методов (между прочим, таких таблиц у одного класса может быть несколько).
За отсутствием подробного описания механизмов работы gcc исследование внутренней структуры экземпляра класса придется выполнять методом проб и ошибок. В связи с этим метод, основанный на создании оболочек, может показаться более предпочтительным, чем способ, описанный в этой статье. Вместе с тем, у непосредственного обращения к методам классов есть свои преимущества: отпадает необходимость в дополнительной разделяемой библиотеке, да и работа программы ускоряется.