В этой статье рассматривается совместное использование Kylix и C/C++ (gcc). Описывается демонстрационная библиотека, позволяющая использовать в Kylix приложении функции Qt library, не определенные в CLXDisplay API (элемент управления QDial). Также приводятся примеры экспорта функций из объектных файлов C/C++ и создания разделяемой библиотеки (shared objects file) в Borland Kylix.
Думаю никто не станет спорить с тем, что наиболее эффективно возможности операционной системы могут быть использованы в приложении, написанном на том же языке, на котором написана и сама операционная система. Это особенно справедливо в отношении Linux, поскольку во-первых, компилятор gcc, с помощью которого компилируется система, входит в ее состав, а во-вторых подавляющее большинство компонентов Linux доступны в виде исходных текстов. Пакет gcc (GNU Compiler Collection) представляет собой мощное средство компиляции программ, написанных на С/C++ и целом ряде других языков программирования. К преимуществам gcc относятся высокая степень оптимизации генерируемого кода, оптимизация вычислений с плавающей точкой, расширяемость и переносимость. Кроме того в gcc/glibc вводится ряд расширений стандартных языков C/C++. Эти расширения облегчают, например, решение таких задач как управление памятью и взаимодействие с операционной системой. С учетом всего выше сказанного возможность совместного использования gcc и Kylix представляется весьма заманчивой.
Среда разработки Borland Kylix предоставляет два способа взаимодействия с программными компонентами, написанными на других языках программирования: экспорт функций из разделяемых библиотек (shared object files) и использование объектного кода, созданного другим компилятором. Главное, на что необходимо обратить внимание при совместном использовании фрагментов, написанных на Object Pascal и C++ -это согласование форматов вызова функций и типов передаваемых параметров.
Согласование типов осуществить довольно просто, отмечу здесь лишь один важный момент. В Kylix, как и в Delphi, переменная типа WideChar занимает два байта оперативной памяти, в то время как соответствующий тип wchar_t, определенный в glibc, занимает четыре байта. Это различие необходимо учитывать при преобразовании типа wchar_t * в тип PWideChar.
Еще одна особенность взаимодействия GNU С/C++ и Object Pascal связана с управлением стековой памятью. В GNU С/C++ определена функция alloca, позволяющая выделять блок памяти в стековой области. В связи с этим могут возникнуть проблемы при взаимодействии с C: если функции, рассчитанной на работу со стековой памятью, передать указатель на блок, выделенный в области динамической памяти, возникает ошибка сегментации. Такая ситуация встречается очень редко, но мне пришлось с ней столкнуться. В Object Pascal нет функции для выделения стековой памяти. Функция GetMem, как и C функция malloc, резервирует память в динамической области. Однако в Object Pascal существует возможность выделять блоки стековой памяти. Для этого следует использовать динамические массивы (массивы переменной длинны). Если в функции объявлена локальная переменная типа "динамический массив", память, выделяемая для этой переменной, будет располагаться в стековой области функции. Следующие два фрагмента на C и Object Pascal существенно эквивалентны:
char *p; p = alloca ( 128 ); somefunc ( p, 128 ); ... |
var p : array of Byte; begin SetLength( p, 128 ); somefunc( Pointer( p ), 128 ); ... |
Разделяемые библиотеки, кроме прочего, позволяют использовать функции, написанные на одном языке программирования, в программах, написанных на другом языке.
В качестве примера рассмотрим разделяемую библиотеку, расширяющую возможности взаимодействия Kylix и Qt library. В состав Qt library входит класс QDial, позволяющий создавать визуальный элемент управления "круговая шкала настройки" (rounded range control). Этот элемент управления не входит в набор компонентов VisualCLX, и CLXDisplay API не предоставляет средств для работы с ним. Для того, чтобы сделать элемент управления QDial доступным в Kylix-приложении, нам потребуется библиотека C++ функций, инкапсулирующих составные элементы класса QDial. Идея подобной библиотеки была изложена в статье Взаимодействие с системой: Linux API и Qt library. По целому ряду причин библиотеку для работы с классами Qt library удобнее всего реализовать в виде разделяемого файла.
Прежде чем мы приступим к анализу процесса создания библиотеки, хочу еще раз обратить Ваше внимание на некоторые юридические нюансы, связанные с совместным использованием Kylix и Qt (см. в конце упоминавшейся выше статьи). Учтите также, что для того, чтобы иметь возможность компилировать программы, использующие Qt library, Вы должны прописать некоторые директории Qt в системных переменных окружения. Подробности приводятся в документации Qt в разделе "Установка".
При создании API для работы с Qt library в Kylix приходится решать вопрос о том, каким способом программист, работающий в Kylix, будет назначать обработчики различных событий Qt. В самом CLXDisplay API для этого предусмотрено несколько методов, которые обсуждались в предыдущих статьях. При реализации нашей библиотеки мы поступим проще и воспользуемся механизмом функций обратного вызова. Разумеется, сигналы и публичные слоты класса QDial также будут доступны в Kylix приложении, однако использование сигналов и слотов в Kylix не очень удобно. Для того, чтобы в ответ на то или иное событие QDial вызывалась соответствущая внешняя функция, назначенная программистом, в C++ библиотеке нам потребуется создать производный класс класса QDial и объявить в этом классе слоты для сигналов QDial. Обращение к функциям обратного вызова осуществляется из функций-слотов. Ниже приводится фрагмент заголовочного файла customqdial.h, в котором объявляется класс CustomQDial, являющийся потомком класса QDial.
class CustomQDial : public QDial { Q_OBJECT public: CustomQDial ( QWidget * parent=0, const char * name=0 ) ; CustomQDial ( int minValue, int maxValue, int pageStep, int value, QWidget * parent=0, const char * name=0 ); void set_cbf_valueChanged ( cbf_valueChanged f ) { OnValueChanged = f; } void set_cbf_dialPressed ( cbf_dialPressed f ) { OnDialPressed = f; } void set_cbf_dialMoved ( cbf_dialMoved f ) { OnDialMoved = f; } void set_cbf_dialReleased ( cbf_dialReleased f ) { OnDialReleased = f; } public slots: void sl_valueChanged ( int value ) { if ( OnValueChanged != 0 ) OnValueChanged ( this, value ); } void sl_dialPressed () { if ( OnDialPressed != 0 ) OnDialPressed ( this ); } void sl_dialMoved ( int value ) { if ( OnDialMoved != 0 ) OnDialMoved ( this, value ); } void sl_dialReleased () { if ( OnDialReleased != 0 ) OnDialReleased ( this ); } private: cbf_valueChanged OnValueChanged; cbf_dialPressed OnDialPressed; cbf_dialMoved OnDialMoved; cbf_dialReleased OnDialReleased; void initobj (); }; |
У класса CustomQDial два конструктора, соответствующих конструкторам базового класса QDial. Четыре функции-члена, начинающиеся с префикса set_, служат для назначения функций обратного вызова. Обращение к функциям обратного вызова осуществляется в обработчиках сигналов - слотах. В конструкторах класса CustomQDial, определенных в файле customqdial.cpp, кроме передачи параметров конструкторам базового класса выполняется связывание сигналов базового класса QDial со слотами класса CustomQDial для создаваемого объекта.
Далее необходимо определить функции, инкапсулирующие обращения к методам объектов CustomQDial. Объявления этих функций содержатся в файле customqdial.h, а определения - в файле customqdial.cpp. Имена функций строятся по той же схеме, что и имена функций CLXDisplay API. Вот как выглядит, например, одна из функций CustomQDial_create, создающая экземпляр класса CustomQDial:
CustomQDial *CustomQDial_create ( QWidget * parent, const char * name ) { return new CustomQDial ( parent, name ); }
Функция CustomQDial_setNotchesVisible позволяет управлять отображением делений на шкале элемента QDial:
void CustomQDial_setNotchesVisible ( CustomQDial *Handle, bool b ) { Handle->setNotchesVisible ( b ); }
В параметре Handle функции передается указатель на экземпляр класса, метод которого нужно вызвать.
Полный текст файлов customqdial.h, customqdial.cpp, а также исходный текст демонстрационного приложения Kylix, использующего элемент управления QDial, можно скачать здесь.
Прежде, чем приступить к компиляции библиотеки нам придется выполнить еще одно преобразование. При объявлении класса CustomQDial было задано несколько новых слотов для сигналов класса QDial. Слоты не являются элементами языка C++, это расширение языка, введенное разработчиками библиотеки Qt library. Для того, чтобы классы со слотами компилировались в gcc, необходимо "расшифровать" описания слотов на языке C++. Для этой цели служит Meta Object Compiler - специальный препроцессор, поставляемый вместе с Qt library. Этот препроцессор необходимо использовать для любого приложения, содержащего Qt расширения языка C++. Meta Object Compiler вызывается командой moc. Аргументом команды является имя входного файла, в котором объявлен класс, содержащий расширения C++. Результат выполнения команды - файл, созданный препроцессором, поступает в стандартный поток вывода. В нашем случае в окне консоли необходимо дать следующую команду:
$ moc customqdial.h > moc_customqdial.cpp
В результате выполнения команды в текущем каталоге появится файл moc_customqdial.cpp. Этот файл необходимо включить в файл customqdial.cpp при помощи директивы
#include "moc_customqdial.cpp"
не удаляя директиву #include "customqdial.h".
Теперь можно приступить к компиляции. Пакет gcc позволяет выполнять компиляцию и компоновку одной командой, но для наглядности мы разделим эти процессы. Для того, чтобы скомпилировать исходный текст библиотеки в файл объектного кода даем команду:
$ g++ -c -fPIC -O2 -I$QTDIR/include customqdial.cpp
Ключ -fPIC задает компиляцию позиционно-независимого кода (position-independent code). Этот ключ необходим при компиляции разделяемой библиотеки. В результате выполнения команды в текущем каталоге должен быть создан файл customqdial.o.
Компоновка библиотеки осуществляется командой
$ ld -shared -soname libcustomqdial.so.1 -o libcustomqdial.so.1.0 -L$QTDIR/lib -lqt customqdial.o
Ключ -soname позволяет задать имя библиотеки (so-name). Это имя хранится в файле библиотеки и именно оно используется загрузчиком при вызове библиотечных функций. Опция -o позволяет задать фактическое имя файла библиотеки. Как правило, это имя основано на so-name.
В принципе созданную библиотеку уже можно использовать в Kylix приложении, но в общем случае требуется еще одно действие - регистрация бибилиотеки. Мы выберем самый простой вариант регистрации - запись каталога библиотеки в системных переменных окружения:
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
Кроме этого, "правила хорошего тона" требуют создать две символические связи (symbolic links):
$ ln -sf libcustomqdial.so.1.0
libcustomqdial.so.1
$ ln -sf libcustomqdial.so.1 libcustomqdial.so
Если Вы собираетесь использовать новую библиотеку в программах, находящихся в разных каталогах, имеет смысл добавить каталог библиотеки в переменную LD_LIBRARY_PATH в файле .profile (или .login, в зависимости от того, какой оболочкой Вы пользуетесь).
Теперь нам необходимо объявить функции, экспортируемые библиотекой, в модуле Object Pascal. Команда nm выводит список имен, экспортируемых Linux модулем. Ниже приводится фрагмет вывода команды nm для нашей библиотеки.
$ nm libcustomqdial.so 0000424c T CustomQDial_addLine__FP11CustomQDial 0000429c T CustomQDial_addPage__FP11CustomQDial 000040dc T CustomQDial_create__FP7QWidgetPCc 0000415c T CustomQDial_create__FiiiiP7QWidgetPCc 000041ec T CustomQDial_destroy__FP11CustomQDial 000043d8 T CustomQDial_maxValue__FP11CustomQDial 000043b0 T CustomQDial_minValue__FP11CustomQDial ... |
Если Вы раньше не программировали на C++, эти имена могут показаться Вам совершенно непонятными. Однако все очень просто. Возможность перегрузки функций в C++ означает, что две разные функции могут иметь одинаковые имена, но различные списки параметров. Для того, чтобы различать имена экспортируемых функций, C++ добавляет к имени каждой функции данные о передаваемых ей параметрах. Мы могли бы определить "удобочитаемые" имена для функций, экспортируемых библиотекой, воспользовавшись, например, псевдонимами функций C++ (aliases), но мы не будем этого делать, так как можем переопределить имена в модуле Object Pascal.
В самом Kylix модуле нет ничего необычного, отмечу только два момента: тип указателя на объект CustomQDial определяется как
CustomQDialH = class ( QRangeControlH );
Такое определение позволяет использовать указатель на объект CustomQDial в функциях CLXDisplay API. Для функций обратного вызова определены два процедурных типа:
TValueCallback = procedure( Handle : CustomQDialH; Value : SmallInt ) cdecl; TNoValueCallback = procedure( Handle : CustomQDialH ) cdecl;
Обратите внимание, что функции обратного вызова должны быть не методами объектов, а глобальными функциями. В принципе, ничто не мешает переписать интерфейс так, чтобы в качестве функций обратного вызова можно было использовать методы объектов (я сделал их глобальными для того, чтобы в приложении, не содержащим TForm, не нужно было создавать "пустой" объект для этих функций). Полный текст модуля customqdial.pas, а также демонстрационное приложение можно скачать по ссылке, указанной выше.
Для того, чтобы использовать в программе, написанной на Object Pascal, функции, написанные на C/C++, часто бывает необязательно создавать разделяемую библиотеку. Object Pascal приложение может быть скомпоновано с файлами объектного кода, написанными на другом языке. В этом случае импортируемые функции становятся частью основного файла приложения.
Рассмотрим небольшой пример. В файле twofuncs.cpp, текст которого приводится ниже, определены две функции: max_i и min_i.
#include <stdarg.h> int max_i ( int num, ... ) { va_list ap; int MaxVal, Val; int i = 1; va_start ( ap, num ); MaxVal = va_arg ( ap, int ); do { Val = va_arg ( ap, int ); if ( Val > MaxVal ) MaxVal = Val; } while ( ++i < num ); va_end ( ap ); return MaxVal; } int min_i ( int num, ... ) { va_list ap; int MinVal, Val; int i = 1; va_start ( ap, num ); MinVal = va_arg ( ap, int ); do { Val = va_arg ( ap, int ); if ( Val < MinVal ) MinVal = Val; } while ( ++i < num ); va_end ( ap ); return MinVal; } |
Функция max_i возвращает значение наибольшего из переданных ей аргументов, функция min_i возвращает наименьшее значение. Особенность этих функций заключается в том, что им можно передавать переменное число параметров (многоточие здесь - элемент синтаксиса C/C++). В первом параметре функций передается число параметров для сравнения. Компиляция файла twofuncs.c выполняется командой
$ gcc -c twofuncs.c
Ниже приводится фрагмент модуля Object Pascal, импортирующего функции max_i и min_i.
{$L twofuncs.o} function max_i( num : Integer ) : Integer; cdecl; varargs; external; function min_i( num : Integer ) : Integer; cdecl; varargs; external; |
Обратите внимание на директиву varargs. Эта новая директива Object Pascal позволяет объявлять функции с переменным числом параметров. Директива varargs введена для большей совместимости Object Pascal с C/C++ и может использоваться только для импортируемых (external) функций, объявленных с директивой cdecl. Из этого следует, что единственный способ определить в Object Pascal процедуру с переменным числом параметров заключается в том, чтобы импортировать такую функцию из C/C++ модуля. Объявление функций с переменным числом параметров позволяет применять такие, несколько экзотичные для Object Pascal, конструкции:
var a, b, c, d, MaxOf3, MaxOf4 : Integer; begin ... MaxOf3 := max_i( 3, a, b, c ); MaxOf4 := max_i( 4, a, b, c, d ); ... |
В заключение отмечу еще одну важную деталь, касающуюся объектных файлов. Объектные файлы генерируются в результате компиляции исходного текста. При этом компоновка модулей не производится, а значит не осуществляется резолюция имен. Это означает, что если в компилируемом блоке объявлена функция, не определенная в этом блоке (например функция стандартной библиотеки), то полученный в результате компиляции объектный файл не будет содержать ни определения функции, ни информации о том, где найти это определение. Если затем этот объектный файл будет включен в модуль Kylix (при помощи ключа {$L ...}), встроенный компоновщик Kylix выдаст сообщение об ошибке:
Unsatisfied forward or external declaration <'functionname'>
Для того, чтобы объектный код, содержащий ссылки на внешние функции (unresolved names), мог компоноваться в Kylix приложении, необходимо сделать соответствующие функции доступными в том Kylix модуле, который импортирует объектный код. Если "ненасыщенные" ссылки указывают на библиотечные функции, в разделе uses Kylix модуля необходимо указать модуль, импортирующий функции из соответствующей библиотеки. Например, если импортируемый C/C++ фрагмент использует функции glibc, в раздел uses Kylix модуля следует добавить модуль Libc. Учтите при этом, что во избежание конфликтов с функциями модулей Kylix, некоторые функции glibc в Libc переименованы. Например, функция sleep переименована в модуле Libc в __sleep. Если C/C++ фрагмент пишется с расчетом на использование в Kylix-программе, объявлять библиотечные функции в C/C++ фрагменте следует под именами, присвоенными им в модулях Kylix:
int __sleep ( int seconds );
Хотя функции с такими именами могут быть "неизвестны" gcc, на этапе компиляции это не вызовет проблем.