Qt-Forth - учебный forth с графикой Qt для Windows и Linux
Учиться никогда не поздно,
тем более классическим алгоритмам.
Главная
Учебник
SPF 4.20 и QT
Алгоритмы
Исходники
DownLoad
Об авторе

  1. Подключение графической библиотеки QT к SPF 4.20
  2. Краткое описание и примеры программ SPF 4.20 + Qt

Подключение графической библиотеки QT к SPF 4.20

Введение. Зачем это нужно.

Форт интересный, необычный язык, однако его консольный вариант не выглядит современным решением. Необходим графический интерфейс, причём работающий и в Windows и в Linux. К тому же, желательно иметь возможность использовать различные, готовые механизмы, как то сетевые интерфейсы, XML, OpenGL и т.д. Все перечисленные возможности, как то кросплатформенность, а так же различные современные алгоритмы содержит библиотека QT. Именно её подключением к форту мы и будем заниматься. Данный пример работы с С++ библитекой, которой является QT, можно рассматривать как вариант общего подхода для работы с динамическими библиотеками C++ содержащими в разнообразных DLL и SO.

В качестве форта будем использовать SPF-4.20. Он доступен для Windows и Linux, имеет открытые исходные тексты, активно поддерживается рускоязычным сообществом. Содержит большое количество отлаженных кросплатформенных библиотек.

Постановка задачи. Необходимость в DLL и вызовах методов.

Итак, наша цель научиться использовать из SPF возможности QT. Хотя теоритически можно использовать С++ библиотеки непосредственно из форта, моделируя работу С++ (пример с.f), но практически это очень сложно. Главные трудности, как мне кажеться, это трдность с подсчетом размера памяти для создаваемого объекта, а так же трудность в создании и перекрытии виртуальных функций в С++ средствами форта. В связи с этим нам придется использовать собственную динамическую библиотеку FQT. Естественно, что для Windows это будет FQT.DLL а для Linux fqt.so Писать её будем на С++.

Я использовал MSVC-6.0 для Windows и gcc для Linux, и соответственно на их синтаксис я буду ориентироваться в примерах. С другой стороны полностью переписать (обернуть в собственные вызовы) QT практически невозможно. По этому, я использовал смешанный подход. Динамическая библиотека FQT (которую мы напишем) используется для создания объектов, перекрытия виртуальных методов, вызова обработчиков событий. Однако с форта, мы будем напрямую вызывать методы объектов QT, прямо по их адресам в DLL-ках. Такой подход позволяет минимизировать объём работ в С++, при создании FQT, и к тому же добавлять методы можно прямо из форта, не прибегая к С++.

Различные типы вызовов. CDECL, WINAPI, THIS-Call.

Для возможности вызова функций из динамических библиотек, нам необходимо иметь возможность организовывать различные типы вызовов непосредственно из SPF. Сам вызов, это CALL (в терминах ассемблера), а типы вызовов, это способ передачи параметров, через стек, через регистры и т.д.

stdcall/winapi - широко используется в Windows
cdecl-call - С++ стандарт
this-call - для работы с объектами С++.

Вызовы типа this-call содержат неявный указатель на данные объекта, передаваемый в вызовах данного типа. Проблема в том, что различные компиляторы по разному организуют данные типы вызовов. Как я говорил выше, мы будем ориентироваться на MSVC 6.0 и gcc. Таким образом нам нужны вызовы:

CDECL-Call      - Стандарт для C++
STDCALL-Call    - Работа с WinAPI
WINAPI-Call     - Работа с WinAPI
Extern          - Работа с глобальными переменными
THIS-CDECL-Win-VC-Call      -  Работа с объектами в MS VisualC 6.0 (windows) 
THIS-CDECL-Linux-gcc-Call   -  Работа с объектами в gcc (linux) 

Для организации вызовов типа CDECL-Call, STDCALL-Call, WINAPI-Call – воспользуемся словами PAS-EXEC и C-EXEC из библиотеки SPF ~ac\lib\ns\so-xt.f Для организации вызова THIS-CDECL-Win-VC-Call воспользуемся словом THIS-CDECL-CALL-ECX написанном на встроенном ассемблере. Алгоритм этого вызова такой же, как и у CDECL-Call, но указатель на объект передаётся в регистре ECX. Для THIS-CDECL-Linux-gcc-Call тоже что и CDECL-Call, но указатель на объект первый в списке аргументов.

Более подробно о вызовах http://www.programmersheaven.com/2/Calling-conventions

Так же следует помнить, что в динамических библиотеках C++ используется искажение имен функций, и метод QWidget::show() будет назван ?show@QWidget@@QAEXXZ или что то похожее.

Модуль загрузки библиотек. Двойственность загрузки библиотеки.

Теперь, когда понятно, как использовать вызовы, нужно знать самое главное, а именно адрес на который передать управление. Файлы DLL (Windows) и SO (Linux) как раз и содержат готовые к применению функции, которые мы и будем вызывать. Обычно, при работе на С++ или других языках программирования, компилятор берет на себя всю работу, по поиску, загрузке и вызову нужных функций. Мы же в SPF всё это будем делать сами.

Программа явно может загружать и выгружать динамические библиотеки. Делает она это при помощи функций API:

Когда библиотека загружена в память, следующим этапом надо найти адреса нужных функций. Динамическая библиотека содержит таблицу имен функций. Есть много утилит которые показывают эту таблицу. Для Linux : objdump -t имя.so например. Адрес функции, это адрес куда надо передать выполнение для выполнения функции. Ищем этот адрес:

Если вызов прошел удачно, то на выходе будет адрес нужной нам функции. По этому адресу можно её вызвать. Перед вызовом нужно положить в стек параметры, а после вызова забрать результат выполнения, если он есть.

При работе в форте, надо учитывать факт того, что программа форта работает как бы в двух режимах. Первый, непосредственно интерпретация программы на форте, а второй – это работа исполняемого модуля (EXE) после компиляции программы. Так вот загрузка динамических библиотек должна учитывать такую двойственность. Я долго не мог понять, почему запуск spf4 Имя.f работает правильно, а EXE модуль Имя.EXE валится по ошибке.

Другими словами, загрузку библиотеки надо осуществлять непосредственно в момент работы готовой программы, перед использованием ей вызова внешних функций.

Для обеспечения условия правильной загрузки, а так же для упрощения читабельности кода на форте и для обеспечения гибкости вызовов применяется несколько слов на форте (Library”, _-Call" и т.д.), реализующих следующий алгоритм:

Обращаю особое внимание, при создании файлов EXE, обязательно перед реальным использованием в EXE вызовов функций из динамической библиотеки, необходимо осуществлять загрузку её в память: LibraryLoad libc (в качестве примера).

Объекты в SPF и обоснование их применимости.

Имея подключенные внешние функции, можно писать любые программы. Но, пользоваться прямо такими вызовами не удобно, громоздко. Воспользуемся библиотекой ~day\hype3\hype3.f для организации объектно-ориентированного (ОО) механизма. Мы должны для каждого созданного объекта QT (C++) запомнить его адрес непосредственно в форте, что бы потом передавать его в качестве аргумента внешним функциям. Все объекты у нас описываются похожим образом. Рассмотрим типичное описание на примере QWidget.

// Базовый класс QWidget, на него замыкаются другие виджеты
CLASS fQWidget
   1 CELLS PROPERTY adr_fQWidget   // Запомним адрес объекта QWidget
: create     // Инициализация класса
   0 СоздатьОкно      // Создать объект QWidget в С++ и его адрес на стек
     adr_fQWidget !   // Запомнить адрес QWidget
   ;
: show  // Показать окно
   adr_fQWidget @ ПокажиОкно DROP  // вызов внеш. функции и сброс кода возврата
   ;
   
// Здесь СоздатьОкно – это вызов внешний функции типа CDECL-Call с 1 параметром:
Library" fqt.dll" libfqt          // Грузанем нашу библиотеку в память
Library@ libfqt  1 CDECL-Call" QT_QWidget" СоздатьОкно

// Здесь ПокажиОкно – это вызов внешней функции QWidget.show().
Library" QtGuid4.dll" QtGui       // Грузанем библиотеку Qt в память
Library@ QtGui  0 THIS-CDECL-Win-VC-Call" ?show@QWidget@@QAEXXZ" ПокажиОкно // ПокажиОкно

Таким образом, вызовы QT мы включаем в нашу объектную иерархию. Так как в QT объекты связаны наследованием, мы тоже будем наследовать объекты. Это сделано для того, что бы можно было использовать методы QWidget (базовый класс) для других клссов, например QTextEdit. Все визуальные классы наследуют метод show, определенный в классе QWidget. Конечно повторить всю QT мы не можем, но в состоянии довольно быстро добовлять нужные методы и классы по мере необходимости.

Архитектура нашей программы.

+----------------------+              +---------------------------------+
|Наша программа на SPF |----явно----->| fqt.dll (наша всп. библ. на C++ | 
+----------------------+              +---------------------------------+
     |                                                     |
     |            +---------------------------------+      |
     +---явно---->| QtGui.dll, QtCore.dll и т.д.    | <----+ неявно через С++
                  +---------------------------------+

Наша программа на SPF взаимодействует с fqt.dll для тех действий, которые тяжело повторить на форте и взаимодействует прямо с QT (QtCore.DLL, QtGui.DLL) для подключения методов (см описание QWidget выше). Обратите внимание, на искажение имён (?show@QWidget@@QAEXXZ) вносимое компилятором С++. В этом имени зашифрован тип вызова и кол параметров. Для разных компиляторов может быть различным. Почему я решил использовать именно такую архитектуру? По тому, что без spf.dll не обойтись, нужно перекрывать виртуальные функции C++ и обрабатывать события, а с другой стороны всю QT запихать в spf.dll нет возможности. К тому же описать в SPF новый метод очень просто и для этого не нужно менять fqt.dll. Это позволяет свести к минимуму работу с C++.

Основное приложение QT. Главная процедура. Ревызов из DLL.

Вот мы и подошли к очень важному, принципиальному моменту. Правильная последовательность вызовов. Честно сказать, я почти полгода не мог найти правильную последовательность вызовов QT. А всё потому, что думал на C++, а не на ассемблере :(. Для того, что бы понять суть, рассмотрим работу с QT из С++.

Типичная (минимальная) программа:  
#include        // Включить в программу файлы заголовков

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);     // Инициализация Qt
	..... работа с графикой ........
    return app.exec();                // Цикл опроса графических событий
}

Как видно, есть всего две функции (QApplication и app.exec), которые как скобки обрамляют основное тело программы. В начале я пытался просто вызвать эти две функции из SPF (смотри http://mgw.narod.ru/algoritms.htm#SPF-QT, и даже что то работало. Однако программа валилась при любой возможности. После исследования работы под отладчиком MSVC 6.0 данного варианта, я понял, что проблема в наборе значений регистров в процедуре main(). Совершенно точно надо было обеспечить сохранность и неизменность регистров EBP и некоторых других между вызовами QApplication и app.exec(). Так как из SPF это сделать проблематично, я подумал, а почему бы не передать вызов непосредственно в main(), а уже из неё вызвать форт. Таким образом алгоритм взаимодействия SPF и QT следующий:

(SPF)                                                  (C++) fqt.DLL
=============                                        =================
: адрSPF  // CALLBACK <-------+             
    Создать QWidget ----------|---------->  СоздатьQWidget() 
    Создать ......            |             {
;                             |                return NEW QWidget();
                              |             }
                              |             
: mainSPF                     |
    адрSPF argv argc          |                               
    ИнициализацияQT ----------|---------->  ИнициализацияQT(argc,argv,адрSPF) 
;                             |             {
                              |               app=QApplication app(argc,argv);
                              ----------------Вызов адрSPF
                                              return app.exec()
                                            }  

Таким образом, ИнициализацияQT в fqt.dll выступает как функция main(), из которой вызываются различные процедуры (SPF). Так как С++ за нас сохраняет всё что ему нужно между вызовами QApplication и app.exec(), то всё сразу стало нормально работать. Приведенное выше взаимодействие с QT работает нормально не только в SPF но и других языках программирования. Следует отметить, что для SPF пришлось модернизировать работу ИнициализацияQT(argc,argv,адрSPF). Прямой вызов не работал и заработал только когда я вставил сохранение регистров.

ИнициализацияQT(argc,argv,адрSPF) 
{
     app=QApplication app(argc,argv);
     _asm{
         PUSH EBP
         PUSH EAX
         }
     Вызов адрSPF
     _asm{
         POP EAX
         POP EBP
         }
     return app.exec()
 }  

Данная последовательность позволяет нам сохранить состояние нужных регистров перед вызовом CALLBACK и востановить их после вызова.

CALLBACK и их параметры. Виртуальные методы в объектах С++

К этому моменту, мы разобрали почти все принципиальные моменты построения связки между SPF и QT. Однако есть ещё одна проблема. Объекты QT используют большое количество виртуальных методов. Программируя на С++ мы должны перекрыть их, что бы обеспечить их функциональность в рамках нашей задачи. Вопрос, как это сделать с форта? Я не знаю. По этому, я просто в fqt.DLL сам перекрываю эти виртуальные методы, заставляя их вызвать функцию (слово) SPF. Адрес для вызова сохраняется в переменных (полях данных или свойствах) конкретного объекта. Посмотрим это на примере организации объекта QWidget.

Пример. Нам надо организовать объект QWidget с дополнительным свойством вызова функции на событие изменение размера самого окна. Почитав документацию к QT видим, что существует виртуальный метод void QWidget::resizeEvent(QResizeEvent *), который будет вызван при изменении размера окна. Т.к. как с SPF перекрыть такой метод я не знаю, то приходится вводить следующую конструкцию в fqt.DLL

class zQWidget : public QWidget
{
    Q_OBJECT
public:
    zQWidget( QWidget* parent = 0 );
    ~zQWidget();

    void *aOnResize;    // Сохраняет адрес слова CALLBACK форта
    void resizeEvent( QResizeEvent * ); // наш обработчик события 
}; 

void zQWidget::resizeEvent( QResizeEvent *a ) // Тело нашего обработчика
{
      // если сохр адрес не нулевой, то вызови функцию в SPF
	if (aOnResize!= NULL) ((ExecZIM_1_0)aOnResize)((void *)a);
} 
// !!! Установить обработчик на resizeEvent
extern "C" FQT_API void QT_QWidget_onresize(zQWidget* qw, void *uk) {
	qw->aOnResize = uk;
}

Таким образом мы ввели в fqt.DLL возможность обработать событие «изменение размера окна». Для правильной обработки этого события из SPF нам надо:

Отладка DLL и SPF. Работа с отладочными словами. INT3

Важная проблема, при разработке такого комплекса, это отладка. Для примера расскажу, как я вёл отладку в MSVC 6.0. Текст библиотеки fqt.DLL написан на С++. Значит нам надо просто вызвать данную библиотеку на выполнение и с SPF вызывать нужные функции. Отладка DLL на С++ описана подробно в Интернете. Нам надо просто во вкладках проекта установить, что основная программа будет SPF с параметрами (spf4 console.f). При запуске на выполнение из IDE MSVC будет сообщение, что SPF не имеет отладочной информации, но нам это не принципиально. Главное, если мы поставим точку остановки на текст DLL – отладчик включится при достижении этого места. Таким образом мы можем контролировать приходящие из SPF параметры и вести отладку нашей fqt.DLL.

Кстати, ни чего не мешает нам вести отладку ассемблерных слов в SPF, используя для этого MSVC (или любой другой отладчик). Что такое «точка останова» в терминах ассемблера? Это просто машинная команда INT 3, встретив которую управление передаётся отладчику. Просто в SPF определяем слово:

CODE BreakPoint           // Прерывание для внешнего отладчика
    INT 3
    RET
END-CODE
	

Теперь встретив это слово, произойдет прерывание и переход в отладчик, где можно спокойно дизассемблировать код, посмотреть память или состояние регистров, выполнить программу по шагам и т.д. и т.п. В Linux похожим образом я использовал отладчик gdb и ddd.

Основной шаблон графической программы.

Теперь, когда мы разобрали основные части нашей библиотеки, поговорим о минимальной программе с использованием графики QT. Но прежде пару слов о подключении DLL или SO. Для Windows всё просто, поместите fqt.DLL в каталог с самой программой (сохраненной по SAVE) или в каталог SPF. Поиск DLL в Windows начинается с каталога программы. А вот в Linux это не так. Там поиск библиотеки происходит по строго определенному маршруту. Для включения в этот маршрут каталога программы я использую переменную окружения:

LD_LIBRARY_PATH=`pwd`; export LD_LIBRARY_PATH

Запуск данной конструкции присваивает переменной LD_LIBRARY_PATH текущий каталог и делает её видимой другим программам. Таким образом, текущий каталог становится доступным для поиска fqt.so которая содержит связку с QT Linux.

Итак минимальная программа должна включать:

Получилось не мало, но и задача сложна. К тому же большинство действий можно описать только один раз, а потом подключать в файле. Я стараюсь писать подробно, что бы вы поняли суть подхода. Ни чего не мешает подключить это всё к любому другому форту или вообще к другому языку программирования. Да и всё равно, это намного короче, чем программировать на Win API, а для Linux вообще реальной альтернативы нет.

Главное, на выходе реальный кросплатформенный текст на SPF одинаково работающий и в Windows и в Linux!

Графическая консоль, как пример работы с библиотекой.

В качестве примера посмотрим на текст графической консоли console.f Для успешной работы с QT надо представлять, как взаимодействуют объекты внутри неё. Так как FQT.DLL всего лишь обертка, она не скрывает внутренности QT. Одним словом, надо понимать, что такое Widget, layout и т.д.

Графическая консоль представляет собой окно типа QMainWindow, в которое вставлен главный выравниватель QLayout, в который в свою очередь вставлены редактор QTextEdit и строка ввода QLineEdit. Так же в QMainWindow вставлена статусная строка QStatusBar. Для строки ввода QLineEdit определён CALLBACK для события нажатия на Enter. Его задача, прочитать строку из строки ввода, скопировать её в окно редактора и выполнить через EVALUATE. Следует обратить внимание на тот факт, что все объекты QT работают со строками типа QString. По этому везде ведется преобразование между обычными строками SPF и QString.

Пример слово
: TYPE_W   // ( As Nstr -- ) Вывести строку SPF в окно QTextEdit
   drop qs1 set       // qs1 объект QString запоминает строку методом set
   qs1 @ te1 append   // te1:QTextEdit забирает qs1:QString и вставляет append
   ;

Так же в тексте программы console.f определены слова для отладки. Это модальный диалог и окно распечатки стека. Они останавливают ход программы и позволяют посмотреть значения на стеке, распечатать дамп памяти или просмотреть значение переменных.

Заключение.

Я этой короткой статье я попытался описать способ подключения графической библиотеки QT к SPF. Осталось неосвещенными множество моментов, как то динамическое создание объектов, конструкторы и деструкторы и т.д. и т.п. Надеюсь, что это еще всё впереди. Пока удалось достичь главного. Форт (SPF 4.20) получил возможность использования современной кросплатформенной графики.

Все исходные тексты открыты и доступны на сайте http://mgw.narod.ru


Краткое описание и примеры программ SPF 4.20 + Qt

Установка библиотеки.

Архив для Windows содержит каталог ~mgw (форт часть) который нужно скопировать в каталог devel SPF. fqt452.dll mingwm10.dll а так же QtCore.dll и QtGui.dll (QtCore.dll и QtGui.dll можно скачать с http://www.mgw.narod.ru/download.htm) - положить в каталог SPF.

Если всё правильно, то можно выполнить console.f - появится окно графической консоли, в котрой уже можно довать команды форта на выполнение.

Шаблон графической программы.

В качестве шаблона графической программы можно использовать ~mgw\example\tutor1.f Рассмотрим его подробнее.

REQUIRE MGW ~mgw\stdlib.f   \ Поддержка QT. Загружает необходимые слова для работы с Qt
fQApplication  NEW app1  \ Обязательная ссылка на QApplication.

\ ----------- Место определения объектов ------------------

fQString       NEW qs1  \ Объект Строка 
fQTextEdit     NEW te1  \ Объект ЭкранныйРедактор
\ NEW фактически равен VARIABLE. В этот момент нет вызова DLL

\ ----------- Место определения CALLBACK обработки сообщений QT ---------

:NONAME  \ ( Aapp -- )  Обязательная Главная процедура работы с QT, тот же main() в примерах на С++
  app1.create            \ Обязательно Запомнить указатель на Application

\ -- Тело программы, инициализация, начальные действия --
  
  qs1.create       \ создадим QString. Вызов DLL - реальный вызов конструктора Qt
  te1.create       \ создадим QTextEdit. Вызов DLL - реальный вызов конструктора Qt
  S" Здравствуй МИР!!!" DROP qs1.set qs1.@ te1.append
  te1.show    \ Отобразим виджет на экране монитора
  
\ ----- Конец инициализации и начальных действий ---------

  app1.@                          \ Положить на стек параметр из DLL
  0                               \ возвращаемое значение (треб SPF)
  ; 1 CELLS CALLBACK: onForth     \ Фактически создали процедуру инициализ Qt
VARIABLE aonForth ' onForth aonForth !

\ ----------- Конец определений CALLBACK обработки сообщений QT ---------

: run  \ Стартовое слово форта
    \ Грузим необходимые DLL. В этот момент заполняются ссылки на функции DLL
    LibraryLoad user32
    LibraryLoad msvcrt           \ Проверка на доступность, нужна для QtGui
    LibraryLoad mingwm10         \ Проверка на доступность, нужна для QtGui
    LibraryLoad QtGui
    LibraryLoad QtCore
    LibraryLoad libfqt
    aonForth @ GetCommandLineA ASCIIZ> args SWAP фСоздатьПриложение
    DROP 
    BYE
    ;

\ ----------- .. ну а дальше, всё как обычно в форте ---------

\ run
    
 ' run MAINX !
  DIS-OPT
  S" tutor1.exe" SAVE 
  BYE

Поддержка объектов построена на библиотеке ~day\hype3\hype3.f. На этой основе сделанн следующий синтаксис работы с объектами:

Класс NEW объект   \ Создать объект в форте
[возможные параметры] объект.create  \ создание объекта в C++
объект.методы                        \ вызов метода объекта

QWidget - Основа основ.

Почти вся работа Qt строится на понятии Widget. Это область на экране монитора. Фактически все визуальные элементы наследуются от класса QWidget. Создание и отображение объектов этого класса в форте fQWidget

Давайте сделаем виджет. Для этого запустим графическую консоль (spf4.exe console.f) из библиотеки. Консоль фактически представляет собой шаблон программы (см шаблон графической программы), в который вставлена функция чтения строки и выполнения её в EVALUATE. За счет этого можно сразу видеть результат выполнения наших команд на форте. Наберем в консоли строку (можно перенести через буфер объмена):

fQWidget NEW окно  окно.create окно.show  \ Создадим окно и отобразим его
На мониторе увидим новое окошко. Посмотрим некоторые свойства данного виджета.
100 200 окно.resize   \ Изменим размер
10  10  окно.move   \ передвинем его в левый верхний угол монитороа

Возникает вопрос, как посмотреть, что ещё можно сделать с окном (фактически с любым виджетом). Для этого смотрим реализацию класса fQWidget (файл ~mgw\stdlib.f) и видим там ещё некоторое количество методов. С простыми методоми всё понятно, но есть и сложные.

Сложные, это когда событие инициирует сама Qt. Событий в Qt много, представлены они виртуальными функциями на C++. В файле fqt452.dll некоторые из них перехвачены и их обработка выведена в форт. Таким образом, для обработки события нам надо указать Qt, куда (на какое слово) передать управление для обработки данного события. Поскольку вызывает Qt в форте эти слова реализованы в виде CALLBACK.

Создадим файл pr1.f

\ Пример работы с fQWidget
\ Вызывать только из графической консоли

fQWidget NEW окно  окно.create окно.show

\ Обработка события OnResize. Для подробного объяснения CALLBACK см. документацию
VARIABLE aonResize
:NONAME  \ ( A -- ) Обработка события resize
  S" ." TYPE
  0
  ; 1 CELLS CALLBACK: onResize ' onResize aonResize !

\ aonResize - это переменная с адресом нашего обработчика
aonResize @ окно.onresize  \ установим обработчик
\ Конец файла pr1.f

При изменении размера нашего созданного окна, в поток выводится точка (S" ." TYPE), что и видно на экране



Hosted by uCoz