Расширение возможностей Kylix приложения: смешиваем Object Pascal и C++


В этой статье рассматривается совместное использование 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, на этапе компиляции это не вызовет проблем.
Исходные тексты библиотеки CustomQDial и демонстрационнного приложения к этой статье.