Bazodanowy program konsolowy pod wrapery w Qt i NCurses - jak?!?

0

Cześć!

Mam problemy filozoficzne... Chcę:

  1. Łączyć rozwój, ciekawe projekty i biznes.
  2. Chcę programować nowoczesne aplikacje w C++ (na pohybel Java w wersji Script!!!).
    2.1 Chcę programować modułowo.

By to wszystko osiągnąć mam zamiar stworzyć apkę edukacyjną (na wszystkie główne platformy czyli Android, Linux i WinDos).
Ta aplikacja nie ma być radosną twórczością nastolatka, tylko przemyślaną i zaprojektowaną apką w stylu aplikacji unix-owych. Jej architektura ma być następująca (numeruję poziomy logiczne):
1: [Moja biblioteka narzędziowa] oraz: [Biblioteka bazy danych]
2: [Biblioteka funkcji bazodanowych i klas zachowalnych]
3: [Program konsolowy]
4: [Nakładka w Qt] oraz: [Nakładka w NCurses]

Tu ważne jest że warstwą biznesową aplikacji ma być program konsolowy, natomiast okienka (ang. forntend) mają być zrobione zupełnie niezależnie. Ma to walory takie, że będę miał czysty silnik apki, bez żadnego syfu oraz skryptami w Bash-u czy Python-ie będę mógł dokładnie go przetestować.

Pierwotnie chciałem się nieco uniezależnić od Qt. Bo do momentu opublikowania tego dokumentu:
https://www.qt.io/blog/2019/08/07/technical-vision-qt-6
coraz dalej się oddalali od korzeni (opętała ich mania własnego języka Java w wersji Script). Ten dokument daje nadzieje że odświeżą warsztat C++ i to znacznie. Jest to o tyle ciekawe, że wcześniej zdawało się że niczego znaczącego w sferze C++ nie będzie.

Jednak cały problem polega na tym, że ta apka konsolowa nie może pracować bez pętli zdarzeń (której czysty C++ nie oferuje). Wynika to z tego, że muszę zapewnić jednoczesną (lub prawie jednoczesną) obsługę wysyłania danych pobieranych w wątkach z bazy danych i obsługę wiersza poleceń - chodzi o to, że wciąganie danych z bazy nie powinno blokować przyjmowania nowych rozkazów (w sumie martwi mnie też to, że wszystko będę pchał do GUI/TUI przez stdout, co prawie na 100% będzie powodowało lagi w aplikacji). To w Qt jest bez problemowe w pewnym sensie, bo zapewnia pętlę zdarzeń w QCoreApplication. Problem jaki obecnie ma Qt jest taki, że wątki nie mogą startować dowolnych funkcji typu std::function<>. Inny problem jest taki, że trzeba dociągać rdzeń bibliotek Qt do projektu.

Pytanie finałowe:

Jak Wy tworzycie aplikacje konsolowe (ew. demony) w C++ wymagające czegoś w stylu pętli zdarzeń?

Jeśli ktoś by powiedział, że powinienem stworzyć demona internetowego zamiast programu konsolowego, to powiem, że to było by strzelaniem z armaty do wróbla. Bo ma to być bardzo prosty programik konsolowy z nieco bardziej wyszukaną obsługą bazy danych (bo w tym kierunku również się rozwijam).

Z góry dzięki i pozdro!
Szyk Cech

0

W Rustcie wykorzystałbym kanały - zdaje się, że do C++ istnieją podobne rozwiązania, na które IMO warto, abyś rzucił okiem.

0

Dla mnie nie jest problemem przesyłanie danych między wątkami. Dla mnie problemem jest zrobienie pętli zdarzeń w czystym C++.

0

O tym właśnie mówię: kanały + async / threadpool.

0

Nie słyszałem o kanałach w C++.
Jednak może mnie oświecisz i powiesz jak w tym Rust byś dodał do tego 2 timery:

  1. Na sprawdzanie czy wątki bazodawnowae się zakończyły.
  2. Na pobieranie danych z std::cin.

Bo te dwie rzeczy muszą się dziać jednocześnie w moim pogrmie.

0

Nie są do tego potrzebne żadne timery.

Ad 1: Również poprzez kanały :-) Uruchomiony w tle wątek mógłby wysyłać wiadomość zwrotną do głównej pętli poprzez specjalnie dla tego wątku utworzony kanał. To całkiem standardowa praktyka przy programowaniu asynchronicznym z wykorzystaniem kanałów.

Ad 2: Osobny wątek przetwarzający dane wejściowe z konsoli i wysyłający je w formie wiadomości do głównej pętli zdarzeń poprzez kanał.

Choć personalnie uważam, że C++ jest średnim wyborem do tego typu usług - jakiś Erlang, Go czy Rust wypadłyby znacznie lepiej (pod względem ergonomii kodu).

0

W ramach ćwiczenia pobawiłem się tym zagadniem.

Zacznijmy od prostego programu Qt:

// test.cpp
#include <QApplication>
#include "widget.hpp"

int main(int argc, char **argv) {
    QApplication app (argc, argv);
    Window window;
    window.show();
    return app.exec();
}
// widget.hpp
#ifndef WIDGET_HPP
#define WIDGET_HPP

#include <QPushButton>

class Window : public QWidget {
Q_OBJECT
public:
    explicit Window(QWidget *parent = 0);
};

#endif
// widget.cpp
#include <QWidget>

#include "widget.hpp"

Window::Window(QWidget *parent) : QWidget(parent), isClosed_(false) {
    setFixedSize(100, 50);
    button_ = new QPushButton("Hello World", this);
    button_->setGeometry(10, 10, 80, 30);
}
######################################################################
# Automatically generated by qmake (3.1) Sun Sep 22 14:16:59 2019
######################################################################

TEMPLATE = app
TARGET = Qt
INCLUDEPATH += .

# The following define makes your compiler warn you if you use any
# feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

# Input
HEADERS += widget.hpp
SOURCES += test.cpp widget.cpp

QT += widgets

To teraz zmontujmy własną pętlę zdarzeń. Jako, że ze mnie leniwa buła jest, to zamiast pisać własną użyję gotowej, mianowicie z Boost-ów. Boost ASIO zawiera takową - wcale nie trzeba pisać aplikacji sieciowej żeby używać. W QT.pro dorzucam

LIBS += -lboost_system

Z test.cpp wyrzucam app.exec() żeby nie włączać GUI i zamiast tego odpalam sobie pętlę zdarzeń ASIO, dla testu wypisuje sobie kropkę. Tutorial do Boost ASIO tutaj.

#include <QApplication>
#include "widget.hpp"

#include <boost/asio/io_service.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>

int main(int argc, char **argv) {
    QApplication app (argc, argv);
    Window window;
    window.show();

    boost::asio::io_service ioservice;
    boost::asio::deadline_timer timer(ioservice, boost::posix_time::seconds(0));

    timer.async_wait([](const boost::system::error_code &){
        std::cout << ".";
    });

    ioservice.run();
}

Fajnie. Teraz wypadałoby to zapętlić. Stack mówi, że trzeba w handlerze przypisać kolejne zdarzenie. No to lecim w nieskończoność:

#include <QApplication>
#include "widget.hpp"

#include <boost/asio/io_service.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
#include <iostream>

int main(int argc, char **argv) {
    QApplication app (argc, argv);
    Window window;
    window.show();

    boost::asio::io_service ioservice;
    boost::asio::deadline_timer timer(ioservice, boost::posix_time::seconds(0));

    std::function<void(const boost::system::error_code &)> checker = [&](const boost::system::error_code &ec) {
        std::cout << ".";
        timer.async_wait(checker);
    };
    timer.async_wait(checker);
    ioservice.run();
}

Stosuję std::function aby móc wołać lambdę rekurencyjnię i łapię przez referencję timer aby móc na nim wywołać kolejny async_wait. To teraz zamiast kropki sprawdźmy zdarzenia w Qt. Dokumentacja podpowiada, że istnieje coś takiego jak app.processEvents(). Fajnie, wkładamy:

    std::function<void(const boost::system::error_code &)> checker = [&](const boost::system::error_code &ec) {
        app.processEvents();
        timer.async_wait(checker);
    };

I już włącza się nam GUI. Problem w tym, że zamknięcie GUI nie wyłącza programu. No to dodajemy handler w Qt:

// widget.hpp
class Window : public QWidget {
// ...
public:
    bool isClosed() const;
private:
    QPushButton *button_;
    bool isClosed_;
};
// widget.cpp

Window::Window(QWidget *parent) : QWidget(parent), isClosed_(false) {
    // ...
}

void Window::closeEvent(QCloseEvent *bar) {
    isClosed_ = true;
}

bool Window::isClosed() {
    return isClosed_;
}
// test.cpp
    std::function<void(const boost::system::error_code &)> checker = [&](const boost::system::error_code &ec) {
        app.processEvents();

        if(!window.isClosed()) {
            timer.async_wait(checker);
        }
    };

Teraz można to sobie rozwijać, dodawać odpowiednie funckje do obsługi bazy czy whatever, ktore będzie można wołać zarówno ze slotu QT jak i handlera od ASIO.

PS: To taki PoC pisany na kolanie, całkiem możliwe, że da się to zrobić lepiej i wydajniej ;)

0

Dzięki za odpowiedzi.
Ogólnie przyjrzałem się programom konsolowym do jakich dobrze się robi nakładki graficzne. Wnioski nie są optymistyczne:

  • programy te bywają językami skryptowymi (Python, MySql) - tam się wrzuca komendę i się ona sama wykonuje.
  • programy te nie posiadają stanu (apt) - wywołuje się serię niezależnych komend bez związku między nimi.
  • programy posiadają jedną centralną funkcjonalność (Mplayer) - wszystkie komendy dotyczą tylko jednego miejsca w programie.

Natomiast ja mam wiele stanów aplikacji - jakby ekranów/okien. To powoduje, że między moim programem konsolowym i nakłądką graficzną musiałaby istnieć ciągła i idealna synchronizacja (tego się boję).
Z tego powodu obecnie składam się do tego, by w miejsce programu konsolowego zrobić klasyczną bibliotekę współdzieloną.
Prędkość wywołań w przypadku biblioteki będzie oczywiście większa, ale w moim programie spowolnienie na obsłudze lini komend była by mało znaczące (jedynie interpretacja komend i ciągła konwersja między std::wstring (UTF-32) i std::string (UTF-8) oraz ew. QString (UTF-16)).

0

Jak dla mnie pomysł z robieniem nakładki GUI do programu CLI jest słabe w przypadku programów innych niż takie, które trzeba wywołać z odpowiednimi parametrami oraz przechwycić co najwyżej wynik.

Jak chcesz się rozwinąć, to faktycznie przemyśl tak architekturę całego systemu, aby w 100% oddzielić logikę od warstwy prezentacji danych. W ten sposób będziesz miał swój "core" który łączy się z bazą, logikę i będziesz mógł w prosty sposób napisać dwa oddzielne interfejsy. Jeden GUI, drugi CLI, czy nawet inny jaki Ci się zażyczy. Do tego właśnie ładnie nadaje się biblioteka dll.

1 użytkowników online, w tym zalogowanych: 0, gości: 1