« Логическое программирование на Пролог. Часть 2 | Про восьмой флеш и физику. Часть 1 » |
Логическое программирование на Пролог. Часть 3
Первым шагом для создания dll для пролога будет создание нужного типа проекта. До этого мы обходились созданием просто файла с исходным кодом *.pro. Создавайте проект через меню “Project/New Project” в свойствах проекта обязательно поставьте, что его тип “dll” и предназначен он для работы под платформу win32.Далее мы начнем как и ранее описывать предикаты, но при этом следует придерживаться определенного правила: как известно ключевыми механизмами пролога являются унификация и backtracking, по этому на любой вопрос который мы задаем, например:
father (X,Y)мы можем получить произвольное количество результатов, от нуля до бесконечности (или чего-то очень большого) . для пролога это вполне нормально, а вот программы на c/Delphi и прочих алгоритмических языках такой неопределенности не поймут. Поэтому нам необходимо при создании предикатов в секции “predicates” выполнять их декларацию как “procedure” – если обратиться к справке пролога, раздел “Модели детерминизма”, то можно узнать, что данное ключевое слово означает что предикат находит одно и только одно решение и при этом не может потерпеть неудачу. Например:
predicates
procedure foo (integer, integer) – (i,i) (i,o)
predicates
nondeterm find_solution (integer , string)
clauses
find_solution (X , "Число положительное или равно нулю"):-
X >=0.
find_solution (X , "Число равно нулю"):-
X =0.
find_solution (X , "Число отрицательное или равно нулю"):-
X <=0.
goal
find_solution (0 , КакоеЧисло).
КакоеЧисло=Число равно нулю
КакоеЧисло=Число отрицательное или равно нулю
3 Solutions
А если мы изменим тип детерминизма на procedure то получим ошибку компиляции.
E;Test_Goal, pos: 407, 590 Nondeterministic clause: find_solutionДля избежания данной проблемы мы вынуждены пользоваться отсечением (cut, !).
predicates
procedure find_solution (integer , string)
clauses
find_solution (X , "Число положительное или равно нулю"):-
X >=0,!.
find_solution (X , "Число равно нулю"):-
X =0,!.
find_solution (X , "Число отрицательное или равно нулю"):-
X <=0,!.
goal
find_solution (0 , КакоеЧисло).
КакоеЧисло=Число равно нулю
1 Solution
Следующим шагом на пути создания библиотеки dll будет создание специального файла определений для экспорта. Ведь согласитесь, что глупо все объявленные предикаты делать общедоступными для пользователей. Если у вы начинали программировать под windows и разрабатывали библиотеки в конце win3.11 и вначале win95, то до появления директив, вроде:
__declspec(dllexport)
Когда мы создали проект типа “dll”, среда Vip5 автоматически сгенерировала данный файл, но к сожалению не добавила его к проекту. Придется нам самим для удобства дальнейшей его правки добавить к проекту. Используйте кнопку “New” в менеджере проекта, и выберите на диске данный файл.
Мы начнем с самого просто, с того, что создадим в прологе предикат, единственной задачей которого будет вывод на экран сообщения.
predicates
procedure say_hello ()
clauses
say_hello:-
write ("Hello From Prolog"),nl.
А вот и еще одна ошибка:
Error 2525: 'tdll1.def' - undefined name ‘say_hello'Нам необходимо все имена на которые мы ссылаемся в *.DEF файле объявлять как глобальные. Для этого заменим директиву “predicates” на “global predicates”. И укажем в объявлении предиката директиву влияющую на правила именования функций: “language stdcall”, окончательно наш код будет выглядеть так:
global predicates
procedure say_hello () – language stdcall
clauses
say_hello:-
write ("Hello From Prolog"),nl.
C tdll1.lib C tdll1.dllДалее мы рассмотрим специфические моменты того, как генерируется код компилятором и общие правила, которые следует учитывать при создании программ, которые содержат фрагменты на разных языках: нам следует разобраться в том, как передаются параметры функциям, как возвращаются результаты из этих функций и как даются имена функциям.
Обычно если функция получает на вход некоторые параметры, размер которых не превышает 32 бита то они передаются через регистры, если передаваемая переменная больше чем 32 бита, то либо она передается в паре eax/edx либо передается указатель на данную переменную. Так как количество регистров ограничено, то передаваемые параметры помещаются в стековую память в определенном порядке, в том же стеке сохраняется адрес точки, куда необходимо вернуть управление после завершения вызова функции. Разные компиляторы по-разному генерируют код в каком порядке передавать параметры через стек и кто должен заботиться об очистке стека после вызова. Компилятор visualC++ различает следующие способы:
Модель | Описание |
---|---|
__cdecl | стек должен очищать тот кто вызвал функцию, параметры помещаются в стек в обратном порядке, справа на лево |
__stdcall | стек очищает сама вызванная функция, параметры передаются через стек и тоже в обратном порядке. |
__fastcall | любители cbuilder сразу вспомнили данную директиву, стек очищается вызванной функцией, параметры по возможности размещаются в регистрах и если только они там не помещаются, то остатки размещаются в стеке – данный способ оправдывает свое название быстрого вызова, работа с регистрами заведомо быстрее чем с памятью. |
thiscall | это не ключевое слово (просто модель вызова) используется при описании членов класса, параметры помещаются в стек, указатель на текущий экземпляр класса (this) хранится в регистре ECX. Стек очищает вызванная функция. |
Следующее что нам следует разобрать – понятие декорации имен. Как известно в c/c++ мы можем описать функции имеющие одно и тоже имя но различающиеся списком параметров. Следовательно компилятор не может их различать по именам, которые им дает программист. Выполняется так называемая декорация имен.
Модификатор PASCAL просто преобразовывал имя функции в верхний регистр. Для модификатора __stdcall принято, что имя функции дополняется спереди символом “_” затем после имени ставится знак “@” и после этого символа пишется число байт занимаемых списком параметров, сколько байт нужно отвести в стеке для передачи данных аргументов. name decoration как пример, если мы объявим функцию как:
int __stdcall func (int a, double b)
_func@12Модификатор cdecl не меняет имя функций в верхний регистр как PASCAL а просто добавляет перед именем символ подчеркивания “_”.
Для того чтобы упростить понимание правил декорации имен попробуйте в visual c++ создать проект (обычный win32console) объявить в нем множество функций с различными модификаторами, а затем посмотреть в “*.MAP” файле новые имена. Для генерации MAP файла надо в опциях линкера (настройки проекта) поставить галку “generate mapfile”
Так если скомпилировать проект, например, с таким кодом:
bool Predicate (char info) {
return islower(info);
}
void Op (char & info){
if (islower(info))
info = toupper (info);
else
info = tolower (info);
}
void main()
{…}
------ Много всего разного ----
0001:000010e0 ?Predicate@@YA_ND@Z 004020e0 f dlq.obj
0001:00001130 ?Op@@YAXAAD@Z 00402130 f dlq.obj
0001:000011b0 _main 004021b0 f dlq.obj
------ Много всего разного ----
Так что мы возвращаемся к visual c++ и создаем новый проект, в котором объявляем некоторую внешнюю функцию, соответствующую тому предикату, который мы экспортировали из пролога.
Для того чтобы избежать декорации имен используйте директиву extern “C”.
extern "C" void __stdcall say_hello (void);
int main(int argc, char* argv[])
{
say_hello ();
return 0;
}
Следующим шагом будет передача в программу на прологе строку текста для некоторой ее обработке, здесь мы просто выведем на экран строку текста:
extern "C" void __stdcall say_it (char *);
...
say_it ("its string passed from c++ to prolog");
say_it (TheString) :-
write (TheString), nl .
add_numbers – для сложения чисел, (здесь мы поработаем с вещественными числами) mod_divide_number – для вычисления остатка от деления одного числа на другое – а здесь с целочисленными переменными.Вот код на прологе (надеюсь в *.DEF файл описания вы сможете сами добавить):
global predicates
procedure add_numbers (real , real , real) -(i , i , o) language stdcall
procedure mod_divide_number (integer , integer , integer) -(i , i , o) language stdcall
clauses
add_numbers (X , Y, Rez):-
Rez = X + Y.
mod_divide_number (X , Y , Rez) :-
Rez = X mod Y.
extern "C" void __stdcall add_numbers (double , double , double *);
int main(int argc, char* argv[])
{
double z;
add_numbers (1 , 2 , &z);
printf ("Z = %lf", z);
return 0;
}
Приятно, что возможность написания имен переменных на русском языке сохранилась, в принципе это ведь всего лишь фрагмент памяти, выделенный в стеке и его имя лишь абстракция, так что иного и не ожидалось, но все равно это приятно, а вот то, что можно именовать по-русски имена предикатов это пожалуй удивительно. Если такого не увидишь то, пожалуй, и не поверишь сразу, вот пример того, как будет выглядеть в depends список экспортируемых символов.
Замечание: depends – просто замечательная утилита, из джентльменского набора любого программиста, позволяет посмотреть список функций, которые экспортируются из некоторой dll, а также ее зависимости от других библиотек. Найти ее можно в стандартной поставке visual studio, примерно в каталоге “C:\Program Files\Microsoft Visual Studio\Common\Tools\DEPENDS.EXE".
Единственное замечание: для того чтобы данный пример с русскими буквами скомпилировался, я использовал в качестве линкера link.exe из поставки visual studio. Если хотите поэкспериментировать, то установите в свойствах проекта “Main program = MSVC++ 32bit”. Кроме этого надо указать путь к каталогу где находится линкер. Делаем это на закладке “Other Options”.
Остается только написать клиентскую часть которая будет вызывать предикаты нашей dll. Здесь нам необходимо уходить от visualc++ ибо попытка объявить функцию которая имеет русское имя никогда не удастся. А вот если сделать прыжок к VBA где и функции и переменные могут иметь русские имена, то может и получится.
Declare Sub AddNumbersVBA Lib "tdll1" Alias "add_numbers" (ByVal A As Double, ByVal B As Double, ByRef C As Double)
Declare Sub Сложи_Два_Числа Lib "tdll1" Alias "сложи_два_числа" (ByVal A As Double, ByVal B As Double, ByRef C As Double)
Declare Sub сложи_два_числа Lib "tdll1" (ByVal A As Double, ByVal B As Double, ByRef C As Double)
Sub zed()
Dim C As Double
Call AddNumbersVBA(1, 2, C)
MsgBox "C = A+B = " & CStr(C)
Call Сложи_Два_Числа(1, 2, C)
MsgBox "Русская версия имени функции работает: C = A+B = " & CStr(C)
Call сложи_два_числа(1, 2, C)
MsgBox "И даже так русская версия имени функции работает: C = A+B = " & CStr(C)
End Sub
cipher (Символ, Смещение, РезультирующийСимвол) :-
char_int (Символ, КодСимвола),
РезультирующийКод = КодСимвола + Смещение,
char_int (РезультирующийСимвол, РезультирующийКод).
global domains
Символ = char
Ключ = integer
global predicates
procedure cipher (Символ, Символ, Ключ) - (i , i , o) language stdcall
extern "C" void __stdcall add_numbers (double , double , double *);
extern "C" void __stdcall cipher (char , int , char *);
int main(int argc, char* argv[])
{
double z;
add_numbers (1 , 2 , &z);
char code;
cipher ('a' , 3 , &code);
printf ("Z = %lf", z);
printf ("cipher = %c", code);
return 0;
}
char – char
real – double
integer – int
symbol, string = char*
в том случае если некоторый параметр возвращается из предиката пролога, то его объявляют как указатель. Далее я привожу пример, как можно получить из пролога строку текста: наша программа, предикат, если быть точным, будет спрашивать пользователя как его зовут, а затем введенное значение будет возвращаться программе на visualc++ которая просто выведет его на экран.
global predicates
procedure get_user_name (string) - (o) language stdcall
clauses
get_user_name (UserName) :-
write ("What is your name ?"),
readln (UserName).
extern "C" void __stdcall get_user_name (char **);
int main(int argc, char* argv[])
{
char * name ;
get_user_name (&name);
printf ("C++: your name is %s" , name);
return 0;
}
dll_mark_gstack(STACKMARK):- STACKMARK=mem_MarkGStack().
dll_release_gstack(STACKMARK):-mem_ReleaseGStack(STACKMARK).
somecallback( String, 0 ):-
dlg_Note("In callback", String),
fail. %<---- free GStack
somecallback( _, 0).
mem_MarkGStack - пометка текущей позиции указателя на вершину GStack,
mem_ReleaseGStack – освобождение памяти, проще говоря позиционирование указателя на сохраненное с помощью предыдущего предиката состояние,
mem_SystemFreeGStack - память которая занимается Gstack и не используется машиной вывода освобождается и передается операционной системе.
Вызывать данные предикаты можно как внутри dll (внутри вызванного извне предиката), например, так:
dll1_custom_Create(Parent,Rct, Id, Win):- % (i,i,i,o)
%"Win" is output parameter, but memory for it is not allocated on GStack
Stackmark = mem_MarkGStack(),
Win = custom_Create(Parent,RCT, Id),
mem_ReleaseGStack(Stackmark).
unsigned long stackmark;
char *Out;
dll_mark_gstack(&stackmark);
getString(&Out);
SetDlgItemText( hDlg, idc_s_ret, Out );
dll_release_gstack(stackmark);
GLOBAL PREDICATES
procedure getString(string Out) - (o) language stdcall
DATABASE - dll_database
single s(string)
CLAUSES
s("Empty").
dll_mark_gstack(STACKMARK):- STACKMARK=mem_MarkGStack().
dll_release_gstack(STACKMARK):-mem_ReleaseGStack(STACKMARK).
setString(In):-
assert(s(In)).
getString(OutStr):- % OutStr is output parameter allocated on Prolog GStack
s(PrologStr),
ASCIIZ_2_VB_String(PrologStr, OutStr). %Convert to BSTR string type. Needed for Visual Basic.
include "types.dom"
IFDEF use_runtime
include "pdcrunt.dom"
include "pdcrunt.pre"
ENDDEF
extern "C" void __stdcall get_user_name (char **);
extern "C" void __stdcall dll_mark_gstack(unsigned long *out);
extern "C" void __stdcall dll_release_gstack(unsigned long in);
int main(int argc, char* argv[])
{
char * name ;
unsigned long pointee;
dll_mark_gstack(&pointee);
get_user_name (&name); // wrapped
dll_release_gstack(pointee);
printf ("\nC++: your name is %s" , name);
return 0;
}
Что же, передавать и получать простые параметры мы уже научились, остается только научиться работать, например, со списками. Как вы знаете списки в прологе одни из наиболее часто используемых типов данных. Я сразу набросал предикат, который выполняет преобразование элементов списка целых чисел, меняя им знак на противоположный.
global domains
ИСписок = integer*
predicates
procedure change_list (ИСписок , ИСписок) - (i , o) language stdcall
clauses
change_list ([] , []):-!.
change_list ([X|Y] , [X1|Y1]):-
X1 = -X,
change_list (Y , Y1).
GOAL
change_list ([1 , 2, -3 , 4, -5], X).
X=[-1,-2,3,-4,5]
1 Solution
extern "C" void __stdcall get_list (int *, int *);
void main (){
double x;
int ix;
char after;
add_number (1 , 2 , &x);
mod_divide_number (10 , 3 , &ix);
cipher ('a' , 3 , &after);
printf("add_number %lf\n" , x);
printf("mod_divide_number %d\n" , ix);
printf("cipher %c\n" , after);
int arr [] = {1 , 2, -3 , 4 , -5};
int arr2 [5];
get_list (arr , arr2);
for (int i = 0; i < 5; i++)
printf ("%d, " , arr2 [i]);
}
Что же не все так, просто может быть кто-то верил что все наши примеры будут с первого раза работать, увы нет. Придется засесть за документацию, благо с прологом идет вполне неплохая справка. Самая большая сложность состоит в том, каков будет заголовок функции на C/C++ соответствующей определенному предикату. И если с именами проблем нет, то вот с входными и выходными параметрами, они уже появляются. В справке идет ссылка на программку с именем pro2c (что-то вроде prolog to c converter), которая и является лекарством, ибо умеет по входному файлу *.pro создать выходной заголовочный файл для c/c++. Правда по закону подлости данного файла в моем дистрибутиве не нашлось (по ходу дела, правда, выяснилось, что данная утилита не распространяется бесплатно вместе с некоммерческой версией пролога (и если скачать дистрибутив, то ее там не будет) а идет вместе с коммерческой, за деньги, разумеется). Так что придется разбираться со справкой без наглядного примера. В ходе данного разбирательства оказалось, что списки принимаются и передаются с помощью ... разумеется тоже списков. Придется вспомнить все, что мы знаем из курса структур данных и алгоритмов. В общем случае под списком понимаем множество узлов хранящих информацию между данными узлами установлены связи в виде указателей для каждого узла на узел предшествующий или последующий за текущим, а вполне возможно и обе ссылки сразу. для того чтобы показать, что следующего элемента нет использовался указатель на NULL. Списки в стиле пролога имеют несколько иное устройство, хотя аналогии очевидны:
вот пример объявления списка целых чисел:
typedef struct i_wrapper {
unsigned char is_eolist;
int data;
struct i_wrapper *next;
} iwrapper;
Для проверки данного утверждения попробуем написать простенький предикат, который подсчитывает количество элементов списка:
get_length_of_list ([] , 0):-!.
get_length_of_list ([H|T] , N):-!,
get_length_of_list (T , N1),
N = 1 + N1.
extern "C" void __stdcall get_length_of_list (i_wrapper *, int *);
void main (void){
iwrapper * pointee_in = NULL;
int number;
do{
printf ("enter number? ");
scanf ("%d" , &number);
iwrapper *iw = new iwrapper;
iw->data = number;
iw->is_eolist = 1;
iw->next = pointee_in;
pointee_in = iw;
}while (number != 0);
iwrapper *iw = pointee_in;
while (iw->next)
iw = iw->next;
iw->is_eolist = 2;
int size;
get_length_of_list (pointee_in , &size);
printf ("size = %d\n" , size);
}
Теперь можно записать фрагмент кода который получает из пролога список целых чисел и выводит его на экран, вернемся снова к задаче с изменением знака всех элементов списка.
На стороне пролога мы пишем:
global domains
ilist = integer*
global predicates
procedure get_const_list (ilist) - (o) language stdcall
clauses
get_const_list ([1,2,3,4,5]).
typedef struct i_wrapper {
unsigned char is_eolist;
int data;
struct i_wrapper *next;
} iwrapper;
extern "C" void __stdcall get_const_list (i_wrapper ** );
int main(int argc, char* argv[])
{
iwrapper * pointee , arr_i [10];
pointee = arr_i;
get_const_list (&pointee);
while (pointee->is_eolist == 1){
printf ("%d, " , pointee->data);
pointee = pointee->next;
};
return 0;
}
И еще пожалуй для демонстрации я приведу пример копии экрана с отладчиком где показывается в watch структура списка.
extern "C" void __stdcall change_list (i_wrapper *, i_wrapper **);
int main(int argc, char* argv[])
{
iwrapper * pointee;
iwrapper * pointee_in = NULL;
int number;
do{
printf ("enter number? ");
scanf ("%d" , &number);
iwrapper *iw = new iwrapper;
iw->data = number;
iw->is_eolist = 1;
iw->next = pointee_in;
pointee_in = iw;
}while (number != 0);
iwrapper *iw = pointee_in;
while (iw->next)
iw = iw->next;
iw->is_eolist = 2;
change_list (pointee_in, &pointee);
while (pointee->is_eolist == 1){
printf ("%d, " , pointee->data);
pointee = pointee->next;
};
iw = pointee_in;
while (iw->next){
iwrapper * iwnext = iw->next;
delete iw;
iw = iwnext;
}
return 0;
}
А вот и проблема проявилась: точнее не проблема, а ошибка, приведенный мною выше код по формированию списка ошибочен. я не заметил, что элементы помещаются в список в порядке обратном требуемому, что же давайте исправим, а может сами сделаете, не глядя на дальнейшее решение.
int main(int argc, char* argv[])
{
iwrapper * pointee;
iwrapper * pre_end = new iwrapper;
iwrapper * the_end = pre_end;
pre_end->is_eolist = 2;
pre_end->data = 0;
// не имеет значение какое число присвоить информационному полю, ведь оно все равно
// будет проигнорировано
int number;
do{
printf ("enter number? ");
scanf ("%d" , &number);
if (number == 0) break;
iwrapper *iw = new iwrapper;
iw->data = number;
iw->is_eolist = 1;
iw->next = the_end;
pre_end->next = iw;
pre_end = iw;
}while (true);
change_list (the_end->next, &pointee);
while (pointee->is_eolist == 1){
printf ("%d, " , pointee->data);
pointee = pointee->next;
};
iwrapper * iw = the_end->next;
unsigned char is_eolist;
do{
is_eolist = iw->is_eolist;
iwrapper * iwnext = iw->next;
delete iw;
iw = iwnext;
}while (is_eolist == 1);
return 0;
}
global database - phonebook
phone (string UserName , string Phone)
clauses
read_phone_book :-
write ("Enter UserName ?"),
readln (UserName),
write ("Enter UserPhone ?"),
readln (UserPhone),
asserta (phone (UserName, UserPhone)),
write ("continue (y/n) ?"),
readchar (Continue),
Continue = 'n',!,
save ("file.base" , phonebook).
read_phone_book:-
nl,
read_phone_book.
GOAL
read_phone_book.
В результате выполнения мы получим текстовой файл со следующим содержимым:
phone("Tatiana","345-67-89") phone("Petro","234-67-89") phone("Vasyan","123-45-67")Теперь давайте попробуем вернуть в получившийся в результате выполнения данного предиката список телефонов людей в программу на c/c++.
Возникает вполне резонный вопрос: а как вернуть не одно значение а множество. очевидно что только в списке. значит нам необходимо найти способ поместить все эти накопленные факты в список. делается это примерно так:
global database - phonebook
dphone (string UserName , string Phone)
global domains
the_phone = phone (string UserName , string Phone)
iphone = the_phone*
predicates
procedure form_phone_list (iphone ListOfPhones) - (o)
nondeterm get_any_phone (the_phone) - (o)
nondeterm read_phone_book ()
clauses
read_phone_book :-
write ("Enter UserName ?"),
readln (UserName),
write ("Enter UserPhone ?"),
readln (UserPhone),
asserta (dphone (UserName, UserPhone)),
write ("continue (y/n) ?"),
readchar (Continue),
Continue = 'n',!,
save ("file.base" , phonebook).
read_phone_book:-
nl,
read_phone_book.
get_any_phone (phone(UserName, UserPhone)):-
dphone (UserName , UserPhone).
form_phone_list (ListOfPhones):-
findall ( PhoneEntry ,get_any_phone (PhoneEntry) , ListOfPhones).
GOAL
read_phone_book, form_phone_list (BigList).
Здесь мы воспользовались стандартным предикатом для Vip5 findall.
Следующим шагом будет передача списка содержащего записи из телефонной книги в программу на C/C++.
global database - phonebook
dphone (string UserName , string Phone)
global domains
the_phone = phone (string UserName , string Phone)
iphone = the_phone*
global predicates
procedure form_phone_list (iphone ListOfPhones, string FileName) - (o, i) language stdcall
nondeterm get_any_phone (the_phone) - (o)
get_length_of_phones_list (iphone, integer) - (i , o)
clauses
get_length_of_phones_list ([] , 0):-!.
get_length_of_phones_list ([H|T] , N):-!,
get_length_of_phones_list (T , N1),
N = 1 + N1.
procedure form_phone_list (iphone ListOfPhones, string FileName) - (o, i) language stdcall
nondeterm get_any_phone (the_phone) - (o)
get_any_phone (phone(UserName, UserPhone)):-
dphone (UserName , UserPhone).
form_phone_list (ListOfPhones, FileName):-
write ("load phones from file " , FileName),
consult (FileName , phonebook),
findall ( PhoneEntry ,get_any_phone (PhoneEntry) , ListOfPhones),
get_length_of_phones_list (ListOfPhones, Size),
nl, write ("Formed List Size = ", Size).
typedef struct phone_tag {
unsigned char fno;
char * UserName;
char * UserPhone;
} phone;
typedef struct phone_wrapper {
unsigned char is_eolist;
phone * data;
struct phone_wrapper *next;
} phonewrapper;
extern "C" void __stdcall form_phone_list (phonewrapper **, char * fname);
void main (void){
phonewrapper * pwr;
printf ("C++ code\n");
form_phone_list (&pwr , "E:\\Programs_Files_2\\prolog_family\\prolog_documents\\vp5\\test_dll1\\Obj\\file.base");
printf ("\nagain C++ code\n");
while (pwr->is_eolist == 1){
printf ("Phone (%s, %s)\n" , pwr->data->UserName , pwr->data->UserPhone);
pwr = pwr->next;
}
}
« Логическое программирование на Пролог. Часть 2 | Про восьмой флеш и физику. Часть 1 » |