Использование объектных файлов Си в Delphi

altСи является очень популярным языком программирования, поэтому библиотек для него создано немереное количество библиотек. А вот библиотек для Delphi — не очень много, поэтому было бы хорошо использовать некоторую часть от огромного количества библиотек напрямую, не переводя код этих библиотек на Delphi. К счастью, Delphi позволяет ссылаться на скомпилированные объектные файлы Си. Но есть проблема «unsatisfied externals» — отсутствующие объявления.

Си – простой, но одновременно – мощный язык, большая часть функциональности которого скрыта в используемых библиотеках. Почти весь необычный и сложный код помещен в функции этих библиотек. Но в базовых библиотеках Delphi нет реализации таких функций. Если просто связать объектный файл Си с необходимым проектом – компоновщик выдаст ошибку «unsatisfied external declarations». К счастью, Си понимает реализацию таких функций, независимо от того, в каком модуле они определены. Если компоновщик сумеет найти функцию с необходимым именем – ее можно будет использовать. Вы можете применять эти возможности для добавления недостающего функционала в проекте на Delphi.

В этой статье будет показано, как собрать и связать объектный файл с модулем Delphi, а также будут представлены все необходимые библиотеки на Си. Я воспользуюсь известным регулярным выражением для поиска кода, которое написал Генри Спенсер из Университета Торонто. Я только немного изменил его, чтобы можно было использовать с компилятором Borland C++. Регулярные выражения немного описаны в справке Delphi, и это очень хороший способ определять модели поиска.

Объектные файлы

Как правило, Си создает объектные файлы, которые должны быть связаны с исполняемым файлом. В Win32 такие файлы, обычно, имеют расширение .obj. Но они бывают разных, несовместимых форматов. Компилятор С++ от Microsoft и некоторые другие, совместимые с ним компиляторы, создают объектные файлы в слегка модифицированном COFF-формате. Поэтому они не могут быть использованы в Delphi. Для Delphi необходимы объектные фалы в формате OMF. Не существует нормального практического способа преобразовать объектный файл из COFF-формата в OMF. Поэтому вам нужен исходный код и компилятор, который генерирует OMF-файлы.

Обратите внимание, что утилита COFF2OMF, которая поставляется со многими версиями С++ Builder не поможет справится с этой проблемой. Эта утилита предназначена только для преобразования библиотеки импорта из одного формата в другой. Библиотеки импорта содержат информацию только о экспортируемых функциях DLL, и могут быть созданы напрямую из DLL, используя IMPLIB или подобные утилиты. Они содержат ограниченное количество тех возможностей, которые есть в полноценных библиотеках на Си или С++. COFF2OMF не сконвертирует объектный файл (или библиотеку) языка Си или С++ в COFF-формат (причины озвучены ниже). Поэтому вам действительно понадобится исходный код и компилятор Borland для создания объектного OMF-файла, для использования его в Delphi.

Автор программ и разработчик Тедди де Конинг утверждает, что COFF2OMF от фирмы DigitalMars может сделать полное преобразование форматов. Но пока я этого не проверял, но говорят – что она стоит своих денег.

Агнер Фог (также разработчик и известный гуру оптимизации) создал утилиту ObjConv, которая сможет преобразовать несколько объектных файлов в другие. Мы прилагаем усилия для создания генератора OMF и не OMF объектных файлов на С++, чтобы их можно было использовать в Delphi.

C++ Builder из состава Borland/CodeGear позволяет создавать такие объктные OMF-файлы. Но не каждый пользователь Delphi имеет и С++ Builder. К счастью, Borland создала компилятор командной строки, который поставляется в свободном доступе вместе с Borland C++ Builder 5. Скачивайте его можно здесь: http://www.borland.com/products/downloads/download_cbuilder.html. Borland уже выпустила шестую версию, так что не ясно, сколько еще бесплатная пятая версия компилятора будет доступна.

Borland / CodeGear выпустила новый бесплатный компилятор с IDE и Turbo C++ Explorer. Хотя он и имеет то же самое имя, но это уже не тот старый продукт Turbo C++, который был выпущен в прошлом веке, это, можно сказать, младший брат BDS 2006, но только для одного языка. Вы можете скачать версию этого Explorer’a, который является полноценной IDE со всем необходимым, и даже лицензию, которая позволяет создавать коммерческие приложения, за исключением возможности устанавливать новые компоненты в IDE (но вы можете использовать эти компоненты в коде). Его можно скачать со страницы закачки Turbo Explorer’a по адресу: http://www.turboexplorer.com/downloads.

Существует ограничение, которое оговаривает – какого рода файлы доступны для использования. Вы можете использовать только объектные файлы, которые были скомпилированы лишь как Си-фалы (а не С++). По некоторым причинам, у компоновщика Delphi существуют проблемы с объектными файлами, которые содержат в себе инструкции на С++. Поэтому, ваши исходные файлы должны иметь расширение «.с», а не «.cpp». Но, поскольку, вы в любом случае не можете использовать напрямую С++ классы – это не является жестким ограничением.

Одно замечание: Си часто использует библиотеку файлов с расширением «.lib». Они просто содержат несколько объектных файлов. Некоторые компиляторы Си поставляются вместе с библиотечными программами для извлечения, вставки, замены или просмотра этих объектных файлов. В Delphi вы не сможете напрямую указывать ссылки на эти .lib-файлы. Но вы можете использовать утилиту TDUMP, которая поставляется с Delphi и С++Builder, чтобы посмотреть – что хранится в этих файлах. Бесплатный С++ компилятор поставляется с библиотечной программой TLIB.

Код

Я не буду обсуждать здесь механизм или принципы использования регулярных выражений. Существует множество материалов, доступных в книгах и в интернете. Но, чтобы использовать их вместе с этим кодом, вы должны знать, что шаблон регулярного выражения – это, своего рода, просто компилятор, который превращает текстовые строки в версии, которые легко можно использовать в коде поиска. Компиляция выполняется в функции regcompile(). При поиске строки в шаблоне регулярного выражения, вы должны передать компилятору сам шаблон и строку в функцию regexec(). Она вернет то место в строке, где обнаружено соответствие текста шаблону (если, конечно, такое соответствие найдено).

Сделать полное описание кода для поиска, с помощью регулярного выражения, довольно сложно и объемно, поэтому я не буду приводить его. Но заголовочный файл, конечно, важен при использовании объектного файла в Delphi. Скачать можно здесь: http://www.rvelthuis.de/zips/cobjs.zip.

/***************************************************************************/
/* */
/* regexp.h */
/* */
/* Copyright (c) 1986 by Univerisity of Toronto */
/* */
/* This public domain file was originally written by Henry Spencer for the */
/* University of Toronto and was modified and reformatted by Rudy Velthuis */
/* for use with Borland C++ Builder 5. */
/* */
/***************************************************************************/
#ifndef REGEXP_H
#define REGEXP_H
#define RE_OK 0
#define RE_NOTFOUND 1
#define RE_INVALIDPARAMETER 2
#define RE_EXPRESSIONTOOBIG 3
#define RE_OUTOFMEMORY 4
#define RE_TOOMANYSUBEXPS 5
#define RE_UNMATCHEDPARENS 6
#define RE_INVALIDREPEAT 7
#define RE_NESTEDREPEAT 8
#define RE_INVALIDRANGE 9
#define RE_UNMATCHEDBRACKET 10
#define RE_TRAILINGBACKSLASH 11
#define RE_INTERNAL 20
#define RE_NOPROG 30
#define RE_NOSTRING 31
#define RE_NOMAGIC 32
#define RE_NOMATCH 33
#define RE_NOEND 34
#define RE_INVALIDHANDLE 99
#define NSUBEXP 10
/*
*Первый байт регулярного выражения внутри «program» на самом деле
*«магическое» число; начало узла — со второго байта.
*/
#define MAGIC 0234
#pragma pack(push, 1)
typedef struct regexp
{
char *startp[NSUBEXP];
char *endp[NSUBEXP];
char regstart; /* Только для внутреннего использования. */
char reganch; /* Только для внутреннего использования. */
char *regmust; /* Только для внутреннего использования. */
int regmlen; /* Только для внутреннего использования. */
char program[1]; /* Только для внутреннего использования. */
} regexp;
#ifdef __cplusplus
extern «C» {
#endif
extern int regerror;
extern regexp *regcomp(char *exp);
extern int regexec(register regexp* prog, register char
*string);
extern int reggeterror(void);
extern void regseterror(int err);
extern void regdump(regexp *exp);
#ifdef __cplusplus
}
#endif
#pragma pack(pop)
#endif // REGEXP_H
Заголовочный файл, описанный выше, определяет некоторые постоянные значения, структуры для передачи информации между кодом регулярного выражения и кодом, который его вызвал. А также – между различными функциями кода и функциями, которые может вызвать пользователь.

#define – это значения констант, начинающихся с приставки RE_, которые были возвращены из тех функций, которые оповещают об успехе или ошибке. NSUBEXP – это число подвыражений, которые можно создать в реализации регулярного выражения. Число, называемое MAGIC, это значение, которое должно присутствовать в каждом скомпилированном регулярном выражении. Если оно отсутствует, структура, очевидно, не содержит действительного скомпилированного регулярного выражения. Обратите внимание, что запись 0234 — не цифровое значение. Начальный ноль говорит компилятору о том, что это восьмеричное значение. Как шестнадцатеричное число использует 16 за основу системы счисления, так десятичное число использует 10, а восьмеричное – 8. Десятичное значение рассчитывается следующим образом:

0234(oct) = 2 * 82 + 3 * 81 + 4 * 80 = 128 + 24 + 4 = 156(dec).

#pragma pack(push, 1) увеличивает текущее состояние выравнивания, устанавливая его на побайтовое выравнивание. Это важно, так как это делает структуры совместимыми с упакованными записями (packed record) Delphi.

Компиляция кода

Если у вас есть С++ Builder или BDS2006 – будет легче скомпилировать код. Вы создаете новый проект и добавляете к нему файл «regexp.c» с использованием пунктов меню «Project», «Add to project» – и компилируете проект. В результате этого, каталог будет содержать файл «regexp.obj».

Если у вас есть компилятор командной строки и он настроен правильно, то откройте командную строку, перейдите в директорию, содержащую файл «regexp.c» и введите:
bcc32 -c regexp.c
Возможно, вы получите предупреждение о неиспользуемых переменных или потере значащих цифр при преобразованиях («unsatisfied externals»), но вы можете сейчас их проигнорировать, поскольку вы код не писали. Я использую этот код уже много лет – и проблем пока не было. После компиляции, вы сможете найти объектный файл «regexp.obj» в том же каталоге, что и исходный файл.

Чтобы сейчас импортировать объектный файл в Delphi, вы должны скопировать его в директории, где находятся исходники на Delphi.

Импорт объектных файлов.

Чтобы использовать этот код в объектном файле, вы должны написать несколько объявлений. Компоновщик Delphi ничего не знает о параметрах функций, о типах регулярных выражений в заголовках, и о значениях, который были определены в файле «regexp.h». Это также не означает, что соглашения о вызовах были использованы. Для этого вы можете написать модули импорта.

Вот интерфейсная часть модуля Delphi, который используется для импорта функций и значений из объектного файла Си в Delphi:

unit RegExpObj;
interface
const
NSUBEXP = 10;
*Первый байт регулярного выражения внутри «program» на самом деле
*«магическое» число; начало узла — со второго байта.
MAGIC = 156;
type
PRegExp = ^_RegExp;
_RegExp = packed record
StartP: array[0..NSUBEXP — 1] of PChar;
EndP: array[0..NSUBEXP — 1] of PChar;
RegStart: Char; // Internal use only.
RegAnch: Char; // Internal use only.
RegMust: PChar; // Internal use only.
RegMLen: Integer; // Internal use only.
Prog: array[0..0] of Char; // Internal use only.
end;
function _regcomp(exp: PChar): PRegExp; cdecl;
function _regexec(prog: PRegExp; str: PChar): LongBool; cdecl;
function _reggeterror: Integer; cdecl;
procedure _regseterror(Err: Integer); cdecl;
Вы заметите, что все функции начинаются со знака подчеркивания. Это все потому, что, по историческим причинам, большинство Си-компиляторов генерируют код, в котором функции начинаются со знака подчеркивания. Чтобы импортировать их, вы должны использовать названия, со знаком «_» вначале. Вы могли бы сказать, что с помощью компилятора С++ Builder можно пропустить подчеркивания, но я, обычно, этого не делаю. Подчеркивания ясно показывают, что мы используем Си-фукции. Они должны быть объявлены с использованием соглашения о вызове в языке Си, который называется cdecl на языке Delphi. Забыв про это — можно получить много ошибок, которые очень трудно отследить.

Оригинальный код Генри Спенсера не имел функций reggeterror() и regseterror(). Я должен был описать их, так как вы не можете использовать переменные в объектных файлах со стороны Delphi напрямую, а код требует возможности сбрасывать сообщения об ошибках в ноль, и получать сообщения об ошибках. Но вы можете использовать переменные Delphi из объектных файлов Си. Иногда объектные файлы требуют, чтобы присутствовали внешние переменные. Если они не существуют, вы можете объявить их где-нибудь в вашем коде на Delphi.

В идеале, часть раздела реализации будет выглядеть следующим образом:

implementation
uses
SysUtils;
{$LINK ‘regexp.obj’}
function _regcomp(exp: PChar): PRegExp; cdecl; external;
function _regexec(prog: PRegExp; str: PChar): LongBool; cdecl;
external;
function _reggeterror: Integer; cdecl; external;
procedure _regseterror(Err: Integer); cdecl; external;
end.
Но если вы скомпилируете этот код, компоновщик Delphi будет жаловаться на отсутствующие объявления («unsatisfied externals»). Модули Delphi должны будут описать их. Большинство функций довольно просты и легко могут быть написаны на Delphi. Только функции, которые принимают переменное число аргументов, такие как printf() или scanf(), нельзя закодировать без использования сборщика. Возможно, если бы вы нашли код printf() или scanf() в бибилиотеках С++, вы могли бы извлечь объектный файл, а также связать его. Я никогда не пробовал делать это.

Коду регулярных выражений необходима библиотечная функция Си malloc() для выделения памяти, strlen() для вычисления длины строки, strchr() для поиска одного символа в строке, strncmp() для сравнения двух строк, strcspn() чтобы найти первой символ подстроки в строке.

Первые четыре функции простые, и могут быть написаны одной строкой на Delphi, так как в ней уже есть подобные функции. Но для strcspn() нет аналогичной функции в библиотеке кода на Delphi, поэтому она должна быть создана с нуля (ручками). К счастью, у меня есть (правда, довольно скверный) код на Си этой функции, только как перевод этой функции в Delphi. В противном случае, я очень тщательно должен был бы прочитать ее спецификацию, и попробовать реализовать ее сам.

Недостающая часть раздела реализации выглядит следующим образом:

// так как этот блок обеспечивает код для _malloc() – он может использовать
// FreeMem и получить PRegExp. Но обычно – _regfree – самое лучшее решение.
function _malloc(Size: Cardinal): Pointer; cdecl;
begin
GetMem(Result, Size);
end;
function _strlen(const Str: PChar): Cardinal; cdecl;
begin
Result := StrLen(Str);
end;
function _strcspn(s1, s2: PChar): Cardinal; cdecl;
var
SrchS2: PChar;
begin
Result := 0;
while S1^ #0 do
begin
SrchS2 := S2;
while SrchS2^ #0 do
begin
if S1^ = SrchS2^ then
Exit;
Inc(SrchS2);
end;
Inc(S1);
Inc(Result);
end;
end;
function _strchr(const S: PChar; C: Integer): PChar; cdecl;
begin
Result := StrScan(S, Chr(C));
end;
function _strncmp(S1, S2: PChar; MaxLen: Cardinal): Integer;
cdecl;
begin
Result := StrLComp(S1, S2, MaxLen);
end;
Как вы можете увидеть, эти функции должны быть объявлены как cdecl и начинаться со знака подчеркивания. Имена функций чувствительны к регистру, поэтому важно их правильно писать.

В моем проекте я не использую этот код. Структура _RegExp содержит информацию, которая не должна быть изменена извне, это не очень удобно. Поэтому я упаковал ее в несколько простых функций, также используя функцию RegFree, которая просто вызывает FreeMem, поскольку с помощью _malloc() я использую GetMem. В идеале, код регулярного выражения должен обеспечить функцию regfree().

Весь исходный код на Си, код модулей для импорта и упакованных модулей, а также очень простой загрузчик программы можно найти на страниц: http://www.rvelthuis.de/downloads.html#cobjszip

Использование msvcrt.dll

Вместо того, чтобы писать все эти функции самостоятельно, вы можете использовать их из библиотеки от Microsoft Visual C++. Это DLL, которые использует Windows, и поэтому они должны присутствовать во всех версиях Windows.

FWTW — это не моя идея. Я получил ее от Роба Кеннеди в группе новостей от Borland. Похоже, что проект JEDI использует эту технику в нескольких своих исходниках.

Используя msvcrt.dll вместо кода, приведенного выше, можно просто объявить большинство внешних процедур. Обязательно используйте имя с подчеркиванием во внешнем объявлении, так как эти процедуры не имеют знака подчеркивания в DLL:

// Обратите внимание, что вы не можете использовать менеджер памяти Си,
// так что вы все равно должны переписать процедуры, как _malloc() в Delphi.
function _malloc(Size: Cardinal): Pointer; cdecl;
begin
GetMem(Result, Size);
end;
// The rest can be imported from msvcrt.dll directly.
function _strlen(const Str: PChar): Cardinal; cdecl;
external ‘msvcrt.dll’ name ‘strlen’;
function _strcspn(s1, s2: PChar): Cardinal; cdecl;
external ‘msvcrt.dll’ name ‘strcspn’;
function _strchr(const S: PChar; C: Integer): PChar; cdecl;
external ‘msvcrt.dll’ name ‘strchr’;
function _strncmp(S1, S2: PChar; MaxLen: Cardinal): Integer;
cdecl; external ‘msvcrt.dll’ name ‘strncmp’;
Это будет работать даже в таких запутанных процедурах, как sprinft() или scanf(). Там, где Си требует дескриптор файла, вы просто объявите указатель. Эффект будет тот же. Для примера:
function _sprintf(S: PChar; const Format: PChar): Integer;
cdecl; varargs; external ‘msvcrt.dll’ name ‘sprintf’;
function _fscanf(Stream: Pointer; const Format: PChar): Integer;
cdecl; varargs; external ‘msvcrt.dll’ name ‘fscanf’;
Я закачал, в некоторой степени — тестовую версию интерфейсного модуля msvcrt.dll на мою страницу загрузки (http://www.rvelthuis.de/downloads.html#msvcrtzip). Я буду обновлять эту страницу, как только будут появляться более протестированные версии модуля.

Проблемы

В то же время, я столкнулся с несколькими проблемами, которые могут быть интересны для читателя. Для преобразования я написал простую тестовую программу на Си, которая использует много процедур, импортируемых из msvcrt.dll. Но оказалось, что некоторые процедуры были не совсем процедурами. Они реализованы в виде макросов, которые непосредственно являются доступными структурами и они не всегда существуют, либо находятся в запутанной мешанине с BCB C, Delphi и Microsoft C.

getchar() и putchar()

Возьмем, к примеру, процедуру getchar(). В stdio.h она заявлена в виде макроса, который доступен для повышения уровня стандартного ввода, и этот стандартный ввод — макрос для &_streams[0]. Если на этом уровне переменная положительная, обычно используют символы из буфера, в противном случае будет использоваться _fgetc() (IOW, __fgetc() на стороне Delphi). Поэтому, независимо от того, как вы объявите собственную структуру, он просто не вызовется.

Это значит, что я должен был объявить __streams и инициализировать уровень поля каким-нибудь отрицательным значением. Проблема заключается в том, что процедуры из msvcrt.dll будут иметь свои собственные версии подобных структур (нет гарантии, что структура FILE там такая же) и они не устанавливают или не читают из BCB процедуры _streams. Поэтому я написал собственную версию __fgetc() для Delphi, которая проверяет, если параметр stream является потоком, также как @__streams[0], указав, что он вызывается с помощью стандартного ввода в стандартный поток ввода. Если это так – это означает, что он вызывается как _fgetch(stdin); который, также как и getchar() – является макросом. Если это так, то прочитайте книги по Delphi, иначе – используйте процедуру _fgetc() из msvcrt.dll.

Я надеюсь, что это будет работать, но я хотел бы знать, если это будет не так. Пожалуйста, напишите мне (на почту ) о возникших у вас проблемах.

FWIW, я отдаю себе отчет о том, что одна из процедур (я думаю — fwrite()) останавливается на брейкпоинте int3 в ntdll.DbgBreakPoint, если ее запустить в отладчике. Если вы нажмете F9 или Выполнить — то программа будет выполнена дальше.

Но процедура puthcar() — тоже макрос, и это опять может повысить уровень. Таким образом, могут возникать такие же проблемы, которые были описаны выше. Я, пока, не встречал их. Но изменения, которые я сделал для getchar(), означают, что, возможно, ungetc() может работать некорректно (AFAICS — это тоже макрос). В случае необходимости, я могу эмулировать всю систему в Delphi. Просто потому, что Си использует несколько макросов.

Это лишний раз показывает, что это не всегда просто — перенаправлять вызовы, в конце концов, к примеру — на mvscrt.dll. Макросы погубили эту идею.

FWIW, макросы — это зло, зло, зло.

fgetpos() и fsetpos()

Похоже, что в msvcrt.dll эти две процедуры хранят дополнительные данные в позициях параметров. В ВСВ позиция — это простое длинное целое. Использование _fgetpos() с объявлением fpos_t в stdio.h BCB вызвало нарушение прав доступа. Так я написал собственную версию этих функций с использованием _fseek() и _ftell().

В теории, fpos_t является непрозрачным типом, и обе процедуры используют указатель на одну. Но в BCB она объявлена как длинная, поэтому она не будет больше, чем четыре байта, а это именно то, что и выделяется. Так что, если msvcrt.dll попытается сохранить больше, чем в нее попытаются записать или часть данных будет перезаписана — то ваша программа не будет работать должным образом или вызовет нарушение прав доступа. Это и есть риск использования кода, который написан с использованием другого компилятора.

Заключение

При условии, что вы мало знаете про Си, и не боитесь сами писать замены для нескольких функций Си (если вы используете msvcrtl.dll — это число будет очень маленьким), связывая объектные файлы Си с модулями Delphi — очень просто. Это позволяет создавать программу, которая не нуждается в DLL и может быть развернута за один раз.

Понравилась статья? Поделиться с друзьями: