Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Расширение возможностей 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, на этапе компиляции это не вызовет проблем.

Проект Delphi World © Выпуск 2002 - 2024
Автор проекта: USU Software
Вы можете выкупить этот проект.