Разработка DLL для CTD с использованием DelphiДжо Мейер (Joe Meyer), Ice Tea Group
Centura Team Developer - великолепный инструмент, который включает множество готовых функций, и почти все, что вам может понадобиться, здесь уже есть. Хорошо, почти все... Время от времени, функциональность CDT приходится расширять. Может потому, что требуется что-то, что не так легко сделать с помощью языка SAL или потому, что вам нужна скорость компилированного кода. Конечно, вы можете начать писать COM-объект, импортировать интерфейс с помощью ActiveX-проводника CTD, затем пройти через весь этот ад вариантного программирования, которое нужно для использования COM в CTD. COM - это не совсем то, чему вас учили? Слышали об этом, но не знаете, как разрабатывать COM-объекты? Хорошо, тогда вы, наверное, предпочтете использовать Visual C++ для написания DLL. Если это вызывает улыбку на вашем лице, и вы чувствуете, как ваши пальцы тянутся запустить Visual Studio, то, возможно, вы не захотите читать эту статью. Если, кроме всего прочего, вы имели дело с Delphi, известной инструментальной средой программирования на языке Pascal компании, тогда вы можете спросить себя, а нельзя ли написать DLL с помощью Delphi и затем использовать эту DLL в CTD? Хорошо, у нас есть две новости: хорошая и плохая. Хорошая новость - Delphi действительно хороша для написания DLL, которые прекрасно интегрируются с CTD. Плохая новость - на этом пути есть несколько ловушек и вы должны знать несколько моментов, чтобы успешно выполнить ваш проект разработки такой DLL. Я начну свою статью со сравнения строковых типов данных, а затем перейду и к другим типам данных. Эти вещи не похожи друг на друга. Строки в Delphi В отличие от почти всех других языков программирования, в Pascal есть специальный тип для строк (тоскливый вздох...). Давным-давно, когда Pascal уже появился, но фирма Borland еще не выпустила первую 32-битную версию Delphi для Windows, длина строки в Pascal была ограничена 255-ю символами. Даже хуже - первый байт строки отводился под ее длину. Новый тип строк появился в первой 32-битной версии Delphi, которая сделала ограничение в 255 символов анахронизмом. Сегодняшняя текущая версия Delphi 6 не ограничивает длину строки (однако это ограничение есть в Windows). К счастью, в Delphi есть и другой тип данных для строк: PChar. PChar - это указатель на символьный массив, совсем как char* в C. В Delphi есть и процедуры для операций с этим типом данных. Строки в C В языке C нет встроенного типа данных, такого как String. Вместо этого строки представляются массивами символов. Завершением строки служит двоичный ноль, следующий за последним символом массива. Библиотеки C включают довольно мало функций для работы с этими специальными массивами символов. Строки в CDT В CDT есть два типа строк: String и Long String. В действительности тип Long String это String , и ведет себя точно так же как String, так что для чего же он хорош? Для ответа на этот вопрос мы должны сделать небольшой экскурс в программирование баз данных. Точнее, в то, как получаются и передаются данные через программный интерфейс базы данных. Как вы, конечно, знаете, базы данных на основе SQL обычно имеют различные типы данных, такие как char и blob. Тип данных char используется для хранения строк определенной длины и может использоваться в выражении WHERE и для объединения таблиц. С помощью полей типа blob вы этого делать не можете. Но что делать, если вы хотите хранить строки длиной более 255-ти символов (ограничение на длину строки зависит от типа SQL базы данных)? Отлично, используйте поле blob и храните столько символов, сколько хотите. Но, так как ничего бесплатного не бывает, вы потеряете возможность использовать эти данные в качестве критерия выбора и не сможете использовать их для индексирования. Почему я вам об этом говорю? Чтобы вам стало ясно, что поля char отличаются от полей blob . Даже хуже, они обрабатываются совершенно по-разному при обмене между клиентом и сервером. Поля blob извлекаются и передаются с использованием специальных внутренних процедур. Для того чтобы сообщить SQL-серверу к какому типу данных вы действительно обращаетесь, вы должны использовать подходящий тип, чтобы дать знать SQL-серверу принадлежит ли эта символьная строка типу char или blob. Именно так вы должны использовать тип String в CTD для char и varchar любой длины, вплоть до максимально поддерживаемой, и использовать Long String для строк, хранящихся в полях blob . Это относится только к выражениям SQL. В чистом языке SAL вы можете использовать любой из типов, это безразлично. Вы можете даже присваивать значения типа Long String переменным типа String. Но есть другая проблема в CTD: это внутренние обработчики для собственного представления строк. Эти обработчики не совместимы ни с какими другими компиляторами. На самом деле, это не проблема, так как при объявлении внешней функции, которая в качестве параметра принимает строку, вы можете просто передавать стандартную строку CTD, и она будет преобразована к типу LPSTR "на лету". Для того чтобы разрешить доступ CTD к библиотекам DLL и передавать и получать строки, вы можете объявить параметр функции в DLL следующим образом: String: LPSTR Это укажет CTD, что нужно конвертировать внутренний дескриптор в стандартный символьный массив C и передать его функции в DLL. Все становится несколько сложнее, когда функция в DLL возвращает строку, и мы рассмотрим этот случай в следующей главе. Дескриптор или указатель? Вот в чем вопрос. В основном, есть два способа объявления строковых параметров, совместимых с CTD: PChar и Handle. Использовать PChar проще всего. CTD будет конвертировать дескриптор в строку C и передавать в DLL параметр типа char*. Так как в Delphi есть совместимый тип, вы можете объявить параметр как PChar в Delphi, и это будет работать достаточно хорошо. Ниже приведен маленький пример, для иллюстрации принципа кодирования: Не так уж и сложно, правда? Но что если вы хотите иметь параметр для возвращаемого значения? Вы можете поступить следующим образом: Delphi: Не так много различий, не правда ли? Да, но только на первый взгляд. При вызове DoNothing в языке SAL вам нужно инициализировать строку, которую вы передаете в DLL. Обычно вы используете SalStrSetBufferLength() для распределения памяти достаточного объема для хранения содержимого, которое будет возвращаться из DLL. Хотя в целом это работает, мне кажется несколько неуклюжей необходимость вызова SalStrSetBufferLength() при каждом моем вызове функции в DLL. Не будет ли изящнее, если Delphi будет принимать внутренний обработчик строки CTD и напрямую его модифицировать? Без необходимости предварительного вызова SalStrSetBufferLength()? Спорю, вы ответите - "да"! Теперь расслабьтесь, или налейте себе стаканчик вина, потому что вам не нужно самому искать решение. Как вы уже знаете, CTD может вызывать функции, размещенные во внешней библиотеке DLL. Но с другой стороны, внешние функции могут вызывать функции из DLL CTD. Круто, правда? Внутреннее представление строк в CTD - это 4-х байтный дескриптор, и мы можем объявить его в Delphi примерно так: Type DLL, экспортирующая функции CTD называется CDLLI20.DLL. Число в имени DLL просто отражает версию CTD. Для версии 1.5 DLL будет называться CDLLI15.DLL. Эта DLL экспортирует несколько функций, используемых для обработки внутреннего представления строк в CTD: BOOL CBEXPAPI SWinInitLPHSTRINGParam(LPHSTRING, LONG); С точки зрения программиста на Delphi это выглядит уродливо, так что давайте посмотрим, сможем ли мы лучше объявить интерфейс к DLL на языке Pascal: const function SWinInitLPHSTRINGParam (var StringHandle:THString; Len:DWORD) : BOOL; stdcall Гораздо красивее, не правда ли? Хорошо, хорошо, мы сделали это красиво, но как теперь с этим работать? SWinInitLPHSTRINGParam - ключ к созданию строк CTD. Эта функция принимает 2 параметра: обработчик и длину. Обработчик будет создан интерпретатором CTD , если вы инициализируете его значением 0 перед вызовом функции. Второй параметр сообщит интерпретатору, сколько байт распределить для реальной строки, и вот как будет выглядеть вызов функции: Var Приведенный пример создает обработчик строки CTD, который указывает на буфер для строки из 200 байт. Отлично, но как поместить текст в этот буфер? Нет ничего проще. Посмотрите на функцию SwinStringGetBuffer. Она принимает 2 параметра. Первый - обработчик, который мы только что создали. Второй возвращает длину буфера, адресуемого этим обработчиком. Результатом функции является то, что нам нужно: указатель PChar прямо ссылающийся на буфер. Этот указатель можно использовать для манипуляций с содержимым строки. Теперь у нас есть все необходимое для создания собственных строк CTD и заполнения их текстом, но что если DLL, написанная на Delphi, получает обработчик и вызывающего ее приложения CTD? Ответ: просто забудьте о функции SWinInitLPHSTRINGParam , так как обработчик уже создан, и, скорее всего, уже содержит текст. Все что вам нужно - это вызвать SWinStringGetBuffer , и вы получите длину строки и, в придачу, указатель на ее содержимое. Хотя все вышеизложенное уже достаточно хорошо работает, я все еще не удовлетворен. Я люблю, чтобы мои функции на Pascal были как можно более гибкими и легко сопровождаемыми, так что я написал несколько инкапсулирующих функций, интуитивно более понятных: interface function SWinCreateString (StringValue:PChar) : THString; overload; procedure SWinSetString (StringHandle:THString; Value:PChar); overload; function SWinGetString (StringHandle:THString) : string; overload; implementation function SWinCreateString (StringValue:PChar) : THString; function SWinCreateString (StringValue:String) : THString; function SWinCreateString (StringLength:DWORD) : THString; procedure SWinSetString (StringHandle:THString; Value:PChar); procedure SWinSetString (StringHandle:THString; const Value:string); function SWinGetString (StringHandle:THString) : string; procedure SWinGetString (StringHandle:THString; var Value:PChar); Приведенные функции привносят смысл в операции создания, присвоения значений и получения строк в различных контекстах. Можно создавать строки путем передачи как PChar , так и собственного типа строк Delphi. Присвоение значения и получение строки тоже можно сделать с использованием как PChar , так и собственного типа строк Delphi. Сами по себе функции не делают ничего особенного, они просто используют SWinInitLPHSTRINGParam и SWinStringGetBuffer для доступа к строкам. Вооружившись приведенными выше функциями, нетрудно обрабатывать строки CTD в Delphi, так что давайте их использовать. Я больше не хочу видеть SalStrSetBufferLength в ваших CTD-приложениях. Число, которое есть число, которое является числом. В CTD есть ровно один тип данных для чисел. В не зависимости от того, есть ли в числе десятичная точка или нет, все равно - это число. К слову, даже логический тип Boolean имеет внутреннее числовое представление. В Delphi для чисел существует несколько различных типов:
Опять же, CTD предоставляет функции и для преобразования внутреннего типа Number во внешние представления числовых типов и из них в Number: BOOL CBEXPAPI SWinCvtIntToNumber(INT, LPNUMBER); BOOL CBEXPAPI SWinCvtNumberToInt(LPNUMBER, LPINT); Так как программисты на языке Pascal ненавидят код, написанный на C, они, возможно, захотят преобразовать эти функции во что-то, вроде этого: function SWinCvtIntToNumber(Value:integer; var Num:TNumber) : BOOL; function SWinCvtNumberToInt(Num:PNumber; var Value:integer) : BOOL; Как вы могли заметить, эти функции используют тип TNumber . Этот тип используется для внутреннего формата CTD для представления чисел - Number , и объявлен так: Type Как видите, TNumber - это запись длиной в 24 байта, предваренная байтом длины. PNumber - просто указатель на тип TNumber. Запись должна быть объявлена с директивой packed. Это гарантирует, что компилятор не будет увеличивать размер байта numLength для выравнивания на границу слова, то есть, не даст компилятору вставить 3 дополнительных байта между numLength и numValue. Байт длины очень важен, так как он может сообщить нам, что число имеет значение NULL. Если число имеет значение NULL , то его длина равна 0. Для удобной проверки этого, я написал следующую исключительно сложную функцию: function NumberIsNull (const Num:TNumber) : boolean; Функция возвращает TRUE (истина) когда число имеет значение NULL , просто проверяя numLength на равенство 0. В действительности процедуры преобразования чисел достаточно просты, но в целях достижения большей ясности я написал две маленькие функции, демонстрирующие их использование: function ShiftLeft (Value:integer) : TNumber; stdcall; function ShiftRight (Value:integer) : TNumber; stdcall; Функции принимаю целый тип данных, побитно сдвигают полученное значение и возвращают результат в виде типа Number . На сегодня это все. Мы узнали, как избежать встреч с чудовищем Visual C++ и приручить красавицу Delphi. Если вы опытный программист на Pascal, все вышеизложенное вообще не должно быть проблемой для вас. |