Расширение возможностей Kylix приложения - смешиваем Object Pascal и C
Оформил: DeeCo
Автор: Андрей Боровский
В этой статье рассматривается совместное использование 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 );
...
Разделяемые библиотеки и C++
Разделяемые библиотеки, кроме прочего, позволяют использовать
функции, написанные на одном языке программирования, в программах, написанных
на другом языке.
В качестве примера рассмотрим разделяемую библиотеку,
расширяющую возможности взаимодействия 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, на
этапе компиляции это не вызовет проблем.
|