Для примера будет использован несложный и бесполезный класс на С++, состряпанный на ходу. В MS VC++ создадим проект, используя MFC AppWizard(exe), без использования представления «Документ-вид», на основе диалога, и обзовем его «example_exe». Добавим два новых файла – example.cpp и example.h.
Файл example.h:В классе есть пара закрытых полей, закрытая функция-член, набор открытых функций. Конструктор принимает два параметра. Строковый параметр будем интерпретировать, как имя объекта. Функция Message нужна для отображения на экране хоть каких-то сообщений, демонстрирующих, что что-то происходит. Proc имитирует процедуру, то есть, не возвращает значения, зато изменяет что-то в программе, в нашем случае, переданный параметр. Func и есть функция, то есть, ничего не изменяет, зато вычисляет некоторое значение и возвращает его. Плюс здесь же установщик и считыватель закрытого поля, а также простенькая демонстрация работы со структурами.
Файл example.срр:Для примера более, чем достаточно. Теперь надо посмотреть, как это работает.
В файле Example_exeDlg.h в описании класса CExample_exeDlg где-нибудь в секции public надо вписать
CExample * ex;
то есть, объявить переменную-член, указатель на наш учебно-тренировочный класс, и в конструкторе Example_exeDlg вписать
ex = NULL;
Можно ex сделать и не членом, в принципе, и инициализировать при объявлении. И, конечно, не забыть наверху этого же файла вклеить заголовочный файл класса:
#include "Example.h"
На диалоговую форму накидаем кнопок и создадим их обработчики:
void CExample_exeDlg::OnBtCreate()
{
if (ex == NULL)
ex = new CExample(7, "Example");
}
Если объект еще не создан – создаем и инициализируем пару закрытых полей.
void CExample_exeDlg::OnBtDestroy()
{
delete ex;
ex = NULL;
}
Освобождаем память и устанавливаем указатель в «пусто»
void CExample_exeDlg::OnBtMessage()
{
ex->Message("Any digit - ", 3);
}
Демонстрационное сообщение.
void CExample_exeDlg::OnBtProc()
{
int k = 5;
ex->Message("before K = ", k);
ex->Proc(&k);
ex->Message("after K = ", k);
}
Показываем в последовательных сообщениях, какое значение переменная имела до выполнения процедуры, и какое стала иметь после.
void CExample_exeDlg::OnBtFunc()
{
int k = 5, l;
ex->Message("before K = ", k);
l = ex->Func(k);
ex->Message("after K = ", k);
ex->Message("Result of Func = ", l);
}
Примерно то же самое – значение до выполнения, значение после выполнения и результат выполнения.
void CExample_exeDlg::OnBtGet()
{
ex->Message("", ex->GetF());
}
void CExample_exeDlg::OnBtSet()
{
ex->SetF(ex->GetF() + 1);
}
Эти две – без комментариев. Должно быть так все понятно... Функцию для работы со структурами в этом проекте не буду трогать, не интересно, тут весь фокус, как их передать через границу DLL. Кроме того, не будем возиться с полями ввода, а передадим параметры непосредственно в коде. Наглядность это уменьшает ненамного, а работы меньше. Еще момент – ID кнопок по-умолчанию поменял с BUTTON1 на BT_CREATE и так далее, для наглядности.
Всё! На форме только кнопки, вывод информации через MessageBox. Можно проверить работу.
Сделаем DLL для этого класса. В MS VC++ создадим проект, используя MFC AppWizard(dll), назовем «example_dll». В каталог этого проекта копируем готовые example.cpp и example.h, добавим их к проекту. Будем изменять, в соответствии с выясненными правилами. Начнем с объявления класса:
// Можно использовать AFX_EXT_CLASS, это синонимы.
Затем из
void Message(CString str, int Digit);
делаем
virtual void __stdcall
Message(CString str, int Digit);
и так со всеми открытыми методами, кроме конструктора и деструктора. И на этом бы всё, да CString – несовместимый, опасный тип. Меняем объявление:
virtual void __stdcall Message (char * str, int Digit);
и определение:
void CExample::Message (char* str, int Digit)
{
//добавляем CString:
CString s = str;
//и немного изменяем работу со строкой:
//str.Format(str + " %d", Digit);
s.Format(s + " %d", Digit);
//this->Show(str);
this->Show(s);
}
то есть, приходим к совместимому типу «указатель на нуль-терминальную строку», но, чтобы не потерять функциональность класса CString, объявляем локальную переменную этого класса и используем ее. Осталось еще полторы детали.
Первая деталь – в файле example_dll.cpp в конце пишем:
// вставляем функцию инициализации..
CExample * __stdcall InitExample(int F, char * N)
{
CExample * ex;
// транслируем конструктору принятые параметры
ex = new CExample(F, N);
// и возвращаем указатель на созданный объект
return ex;
}
// ..и ликвидации
void __stdcall
DelExample(CExample * ex)
{
delete ex;
}
И половина детали – в файле EXAMPLE_DLL.def в конце дописываем пару строчек, так, чтобы получилось:
;*****************************************************************************
; EXAMPLE_DLL.def : Declares the module parameters for the DLL.
LIBRARY "EXAMPLE_DLL"
DESCRIPTION 'EXAMPLE_DLL Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
InitExample
DelExample
;*****************************************************************************
После компиляции DLL готова. Подготовим проект в Delphi, чтобы продемонстрировать ее работу. Создадим проект «Example_Delphi», и в модуле главной формы, перед объявлением класса формы, впишем четыре типа. Два - структуры struct1 и 2:
TRec1 = record
n : integer;
i : integer;
j : smallint;
k : shortint;
end;
TRec2 = record
n2 : integer;
a : array[0..2] of smallint;
end;
Третий – указатель на вторую структуру:
PRec2 = ^TRec2;
А четвертый – наш класс, с которым будем работать:
TExample = class
public
procedure Mess_(str : PChar; Digit : integer);
virtual; stdcall; abstract;
procedure Proc(var Digit : integer);
virtual; stdcall; abstract;
function Func(Number : integer): integer;
virtual; stdcall; abstract;
procedure SetF(F : integer);
virtual; stdcall; abstract;
function GetF(): integer;
virtual; stdcall; abstract;
function Struct1to2(rec1 : TRec1): PRec2;
virtual; stdcall; abstract;
end;
Директивы virtual и stdcall в пояснениях не нуждаются. О них сказано выше. А зачем abstract? Очень просто. Без нее компилятор будет ругаться на неправильное упреждающее объявление функции, ведь описания ее у нас нет, описание – в DLL. Директивы должны идти именно в этом порядке. Иначе компилятору не нравится.
Обратите внимание на первый метод. Остальные названы так же, как и в С++, но слово Message в Delphi зарезервированное, и использовать его не по назначению не стоит. Хорошо, назовем иначе, важно, что она стоит на первом месте среди виртуальных функций, как и в С++, значит, ее найдут по номеру в VMT. Имя роли не играет.
Еще надо добавить объявление экспортируемых функций создания/ликвидации, в конце секции interface: function InitExample(F: integer; N : PChar) : TExample; stdcall;Здесь предполагается, что DLL лежит там, где и появилась после компиляции, а директории «Example_dll» и «Example_Delphi» имеют общую родительскую. Больше нигде ее искать не будут. Если же указать только имя, приложение будет искать библиотеку в своей папке, папках WINDOWS, SYSTEM32 и прописанных в переменной окружения PATH. Впрочем, это азбука.
Всё, класс можно использовать. Давайте опять наделаем кнопок, а вывод в поле Memo, благо, в Delphi с ним работать быстрее и проще, чем в MS VС++.
Вот обработчики кнопок: procedure TForm1.Button1Click(Sender: TObject); begin if not Assigned(Self.ex) then Self.ex := InitExample(10, 'Ex_Delphi'); end; procedure TForm1.Button2Click(Sender: TObject); begin DelExample(Self.ex); Self.ex := nil; end; procedure TForm1.Button3Click(Sender: TObject); begin Self.ex.Mess_(PChar('Некоторая цифра – '), 5); end; procedure TForm1.Button4Click(Sender: TObject); var j : integer; begin j := 15; Self.Memo1.Lines.Add('j До – ' + IntToStr(j)); Self.ex.Proc(j); Self.Memo1.Lines.Add('j После – ' + IntToStr(j)); end; procedure TForm1.Button5Click(Sender: TObject); var j : integer; begin j := 20; Self.Memo1.Lines.Add('j До – ' + IntToStr(j)); Self.Memo1.Lines.Add('Результат – ' + IntToStr(Self.ex.Func(j))); Self.Memo1.Lines.Add('j После – ' + IntToStr(j)); end; procedure TForm1.Button6Click(Sender: TObject); begin Self.Memo1.Lines.Add(IntToStr(Self.ex.GetF)); end; procedure TForm1.Button7Click(Sender: TObject); begin Self.ex.SetF(Self.ex.GetF + 1); end;То же самое, что и в С++, и работает так же. Что и требовалось. И добавим кнопку для функции, которая принимает и возвращает структуры. Вот ее обработчик:
procedure TForm1.Button8Click(Sender: TObject); var s1 : TRec1; s2 : PRec2; begin // здесь компилятор будет ругаться, но в данном // случае это не важно. Просто посмотрим, что // до инициализации s2 - это всякая чушь... Self.Memo1.Lines.Add('s2 до:' + #9 + IntToStr(s2.n2) + #9 + IntToStr(s2.a[0]) + #9 + IntToStr(s2.a[1]) + #9 + IntToStr(s2.a[2]) ); // инициализация s1 s1.n := 10; s1.i := 1; s1.j := 2; s1.k := 3; // если функция возвращает указатель на запись (структуру) - // надо подготовить указатель. Это вам не класс. // s2 - типа PRec2, а не TRec2 s2 := Self.ex.Struct1to2(s1); // ... а потом - то, что мы требовали. Self.Memo1.Lines.Add('s2 после:' + #9 + IntToStr(s2.n2) + #9 + IntToStr(s2.a[0]) + #9 + IntToStr(s2.a[1]) + #9 + IntToStr(s2.a[2]) ); end;Что делает функция – понятно, тут другая тонкость. Конструктор возвращает (в коде на С++) указатель на класс, а мы присваиваем возвращаемое значение переменной, которая, вроде бы, не указатель. Struct1to2 тоже возвращает указатель – и его надо подготовить. Это объясняется в []: «Объект – это динамический экземпляр класса. Объект всегда создается динамически, в «куче», поэтому ссылка на объект фактически является указателем (но при этом не требует обычного оператора разыменования «^»). Когда вы присваиваете переменной ссылку на объект, Delphi копирует только указатель, а не весь объект. Используемый объект должен быть освобожден явно.»
А в С++ структура отличается от класса несколько меньше, и работа с ними почти одинакова.И еще пара тонкостей. Если в DLL добавить еще виртуальную функцию-член, обязательно в конце, после имеющихся, такая DLL будет совместима со старой программой, где в абстрактном классе эта функция не объявлена. И если изменить имеющуюся функцию, добавив в конец параметров параметр по-умолчанию, такая DLL будет совместима со старой программой, где в абстрактном классе эта функция не имеет такого параметра.
Разумеется, можно вынести описание абстрактного класса, объявление экспортируемых функций, используемых типов и тому подобное в отдельный модуль. Возможно, это лучше, чем запихивать всё в один файл. По крайней мере, я так и делаю. Но это уже детали, касающиеся стиля, а не функциональности.