вторник, 29 октября 2013 г.

Подводные грабли, или проблемы несовместимости

При анонсе Qt5 было сказано много громких слов о том, как легко будет тем, кто раньше пользовался Qt4, перейти на новую версию. Слова оказались не так чтоб совсем пустыми, но местами пустоватыми. Рассмотрю один из случаев, с которым имел дело на днях.

Вот как всё было. Некая система, состоящая из сервера и соединяющихся с ним клиентов, была в меру сил протестирована (работаю над ней один, к сожалению) и запущена в работу. Казалось бы, всё работало нормально. Но нет, показалось. Вдруг на машине заказчика в процессе регистрации в системе появилась ошибка, которой при тестировании не было. Как так? Да, у меня возник такой же вопрос. Клиент якобы передавал серверу "недействительные данные". Включаем дедукцию - ага, регистрация тестировалась на клиенте, собранном под Qt4, а в настоящее время используется клиент, собранный под Qt5. Сервер же собран под Qt4. Ну и в чём проблема? Пока не ясно.

Идем дальше. Ставим отладочные сообщения в места, где происходит передача и получение данных, и... данные вроде бы есть, но десериализуются в пустой QVariantMap, хотя должен быть словарь с несколькими парами ключ-значение. Вот оно что! Подозрение сразу падает на сериализацию QUuid, т.к. в Qt4 объекты этого класса "из коробки" не сериализуются, требуется зарегистрировать данный тип вручную. Создаем тестовый проект, пробуем сериализовать QUuid под Qt5, сохранить, прочитать под Qt4, и не можем. Вот вам и простота, и легкость, и беспроблемность, и ещё много столь же громких и столь же пустых слов.

Но всё же — отчего так происходит? Пять минут копания в исходниках Qt — и ответ готов. Qt сериализует типы, зарегистрированные "из коробки", с указанием только их идентификатора (беззнаковое целое, 4 байта), а пользовательские типы — с указанием ещё и их имени (идентификатор при этом равен QMetaType::User). Не трудно догадаться, что происходит при попытке под Qt4 десериализовать данные (не зарегистрированные "из коробки" в этой версии), сериализованные под Qt5: встречаем идентификатор (в случае с QUuid - 30), естественно, не находим его (QMetaType::User равен 1024 в Qt5 и 256 в Qt4, что в любом случае больше 30). Создаём пустой объект. Всё. Данные потеряны.

Что тут можно сказать? Плохо, очень плохо. Для такого большого, серьёзного проекта, как Qt, подобное, на мой взгляд, недопустимо. Тем более, что пути решения есть, и они элементарны. Можно было с самого начала всегда указывать вместо идентификатора название типа для всех типов данных. Самую малость медленнее, зато совершенно надёжно. Ну, не сделали с самого начала, не предусмотрели, — ладно. Второй выход — добавить (скажем, в Qt 4.8.0 или 4.9.0, буде его когда-нибудь выпустят) перегруженный метод qRegisterMetaType, который бы принимал помимо имени типа ещё и идентификатор, чтобы можно было вручную зарегистрировать тот же QUuid с идентификатором 30. Тогда, опять же, проблем совместимости удалось бы избежать. Согласен, не самое чистое решение, но это лучше, чем совсем ничего.

P.S.: Возможно, кто-то задастся вопросом: а что это такое автор хранит и передаёт в виде QUuid? Инвайт-коды, коды восстановления аккаунта и т.д. Почему QUuid? Потому что достаточно трудно подобрать, легко генерируется в одну строчку, сериализуется (тут должен быть нервный смех). Не уверен, что это хорошее решение, но для моего проекта оно вполне подходит.

среда, 14 августа 2013 г.

Ещё немного о переводах в Qt: то, что могло бы быть предусмотрено разработчиками

Система переводов в Qt, на мой взгляд, является одной из наименее проработанных частей этого фреймворка. Да, имеются все необходимые возможности, но не хватает некоторых мелочей. Вместо тысячи слов: QT_TRANSLATE_NOOP3

Для чего нужен этот макрос? Он позволяет инициализировать массив структур, состоящих из исходной строки и комментария (параметры функции tr). При этом исходные строки будут помечены для перевода. Если в дальнейшем передать их функции tr, то они будут переведены. Данный макрос может быть использован только для инициализации переменной-массива. Подобные решения, на мой взгляд, ну никак не тянут на серьёзные. Больше похоже на временную заплатку, словно у разработчика не было времени хоть сколько-нибудь подумать.

Не пускаясь в абстрактные рассуждения и высокие материи, рассмотрим вполне реальный пример. Допустим, нам нужно показывать пользователю определённые сообщения в ответ на ввод соответствующих команд (например, в справочной системе консольного приложения). Конечно, если это отдельное приложение, то можно просто захардкодить нужные строки, обёрнутые в функцию tr. Но если данная возможность реализована в библиотеке и используется несколькими разными приложениями, то такой вариант не пройдёт. Можно, конечно, использовать виртуальные методы, но такой подход, на мой взгляд, излишне громоздкий. Гораздо более естественно создать словарь, в котором команде будет сопоставляться строка, а затем пополнять этот словарь. Вот как это может выглядеть:
 QMap<QString, QString> map;  
 map.insert("command", "This is a command. It does nothing.");  
Отлично, осталось сделать строку переводимой. Ясно, что средствами Qt тут ничего хорошего добиться не удастся. Поэтому предлагаю использовать вот такой простенький класс:
 class Translation  
 {  
 public:  
   static Translation translate(const char *context, const char *sourceText, const char *disambiguation = 0, int n = -1)  
 {  
   if (n < 0)  
     n = -1;  
   Translation t;  
   mcontext = context;  
   msourceText = sourceText;  
   mdisambiguation = disambiguation;  
   mn = n;  
   return t;  
 }  
 public:  
   explicit Translation()  
   {  
     mn = -1;  
   }  
 public:  
   QString Translation::translate() const  
   {  
     QCoreApplication::translate(mcontext.toUtf8().constData(), msourceText.toUtf8().constData(), mdisambiguation.toUtf8().constData(), mn);  
   }  
 private:  
   QString mcontext;  
   QString msourceText;  
   QString mdisambiguation;  
   int mn;  
 };  
Как известно, если обернуть строку в функцию translate, то она будет помечена к переводу. Не важно, в каком пространстве имён находится эта функция. В классе Translation использовано данное обстоятельство. Экземпляр класса создаётся посредством вызова статического метода translate. То есть этот класс — просто обёртка, хранящая исходную строку, которую в любом месте программы можно перевести с использованием QCoreApplication::translate. Конструктор копирования и различные операторы в коде отсутствуют, но подразумеваются.

Пример использования:
 //создаём словарь и пополняем его  
 QMap<QString, Translation> map;  
 map.insert("command", Translation::translate("Some context", "This is a command. It does nothing.", "Some comment"));  
 //выводим соответствующую строку при вводе команды  
 QTextStream in(stdin, QIODevice::ReadOnly);  
 QString command = in.readLine();  
 QTextStream out(stdout, QIODevice::WriteOnly);  
 out << map.value(command).translate();  
На выходе получим переведённую строку. Казалось бы, мелочь, а весьма удобно и естественно по сравнению с этим ужасным макросом QT_TRANSLATE_NOOP3 и по сравнению с использованием виртуальных методов. Есть, конечно, некоторый проигрыш в скорости и объёме памяти, но... как говорится, не смешите мои тапочки. :)

понедельник, 15 июля 2013 г.

Доступ к protected извне: ловкость рук и никакого мошенничества

Не так чтоб каждый день, но иногда всё же требуется получить доступ к protected методам какого-то класса. Простейший пример — QPlainTextEdit. Сразу несколько методов, непонятно для чего сделанных защищёнными (тот же firstVisibleBlock(), например). Конечно, в теории следует наследоваться от базового класса, и таким образом получить доступ. Но не всегда есть возможность это сделать (если, скажем, базовый класс создаётся в какой-то части системы, которая ничего не знает о классе-наследнике), да и не всегда нужно. На днях у меня как раз возникла такая ситуация, поэтому спешу поделиться решением (найденным в интернете, честно говоря, но несколько доработанным).

Итак, простейший случай: метод не перегружен. Тут всё более-менее просто. Смотрим пример:
 class A  
 {  
 protected:  
   void f() {}  
 };  
   
 class B  
 {  
 public:  
   void g()  
   {  
     A a;  
     //делаем что-то с экземпляром A  
     //а вот теперь небольшой фокус  
     struct AHack : A  
     {  
       using A::f;  
       static void fHack(A *a)  
       {  
         if (!a)  
           return;  
         (a->*&AHack::f)();  
       }  
     };  
     //теперь вызываем защищённый метод  
     AHack::fHack(&a);  
     //продолжаем работу  
   }  
 };  
Вот такой трюк с приведением типов. Хоть мы и наследовались от базового класса, но при вызове метода не создавали новых экземпляров.

Теперь представим, что метод A::f() перегружен:
 class A  
 {  
 protected:  
   void f() {}  
   void f(int) {}  
 };  
Тут компилятор выдаст приблизительно такую ошибку:
 ... ошибка: '& A::f' cannot be used as a member pointer, since it is of type '<unresolved overloaded function type>'  
Что делать? Как говорится, "we need to go deeper" (кто не в курсе — погуглите). Слегка изменим нашу структуру AHack:
 struct AHack : A  
 {  
   //using больше не понадобится  
   static void fHack(A *a)  
   {  
     if (!a)  
       return;  
     reinterpret_cast<AHack *>(a)->f();  
   }  
   static void fHack(A *a, int i)  
   {  
     if (!a)  
       return;  
     reinterpret_cast<AHack *>(a)->f(i);  
   }  
 };  
Такой способ является универсальным и будет работать как в случае с перегруженным методом, так и в случае с неперегруженным. Теперь можно вызывать метод A::f() в обоих вариантах:
 AHack::fHack(&a);  
 AHack::fHack(&a, 10);  
К reinterpret_cast обычно прибегать не рекомендуется, так как малейшая ошибка запросто может привести к падению программы, а найти эту ошибку будет довольно сложно. Если бы мы привели тип A * к типу AHack * и использовали бы какой-то метод, отсутствующий в A, то с большой долей вероятности программа бы упала. Но поскольку мы обращаемся только к методам A, то такое приведение типов можно назвать безопасным (но только в данной ситуации). Подробнее о приведении типов можно почитать в одном весьма и весьма интересном блоге, а у меня на сегодня всё.

понедельник, 8 июля 2013 г.

Перевод на лету: так ли всё просто, как кажется?

Сегодня мне хотелось бы рассказать о некоторых приёмах работы с системой перевода интерфейса в Qt. На первый взгляд может показаться, что всё предельно просто (тривиальные примеры в документации этому способствуют). Ну в самом деле, что сложного? Обёртываем строку в tr() или translate(), генернируем файл .ts, переводим, собираем в файл .qm, загружаем его — вот и всё. Но практика, как известно, зачастую несколько расходится с теорией.

Итак, переходим к примеру. Допустим, есть некое приложение, интерфейс которого должен быть переведён на пару-тройку языков. При этом к нему прилагается библиотека, в свою очередь также содержащая элементы, нуждающиеся в переводе. Итого: для каждого языка (кроме, возможно, исходного английского) имеется набор из трёх файлов переводов: appname_xx.qm, libname_xx.qm и (чтобы текст на кнопках подтверждения, отмены и т.д. также был на выбранном языке) qt_xx.qm (тут xx — имя локали для выбранного языка). Это ещё довольно простой случай, ведь иногда основной проект может зависеть от нескольких вспомогательных библиотек, и тогда файлов переводов может быть больше.

Уже посложнее, чем пример из документации, верно? А если мы, к примеру, хотим предоставить наиболее продвинутым пользователям возможность самим переводить интерфейс на их родной язык? В таком случае обычно предлагается помещать нужный файл переводов в соответствующую папку в домашнем каталоге. Итак, что мы имеем: несколько файлов переводов для разных частей приложения, расположенных в разных местах. Их обнаружение и загрузка — дело уже далеко не такое простое, как загрузка одного единственного файла перевода с известным путём.

Что же делать? Выход, конечно же, есть — создать класс, который брал бы на себя обязанности по обнаружению, загрузке и дальнейшему использованию переводов. Начнём, пожалуй:
 class Translator  
 {  
 private:  
   QString mfileName;  
   QLocale mlocale;  
   bool minstalled;  
   QList<QTranslator *> mtranslators;  
 public:  
   QString fileName() const  
   {  
     return mfileName;  
   }  
   QLocale locale() const  
   {  
     return mlocale;  
   }  
   bool isValid() const  
   {  
     return !mfileName.isEmpty();  
   }  
   bool isInstalled() const  
   {  
     return minstalled;  
   }  
   void install()  
   {  
     if (minstalled || !isValid())  
       return;  
     QStringList paths; //в эту переменную добавляем все пути к переводам (то есть список абсолютных путей к папкам)  
     foreach (QString path, paths)  
     {  
       QTranslator *t = new QTranslator;  
       if (t->load(mlocale, mfileName, "_", path, ".qm"))  
       {  
         mtranslators << t;  
         QCoreApplication::installTranslator(t);  
       }  
       else  
       {  
         delete t;  
       }  
     }  
     minstalled = true;  
   }  
   void remove()  
   {  
     if (!minstalled)  
       return;  
     foreach (QTranslator *t, mtranslators)  
     {  
       QCoreApplication::removeTranslator(t);  
       delete t;  
     }  
     mtranslators.clear();  
     minstalled = false;  
   }  
   void setFileName(QString fn)  
   {  
     bool wasInstalled = isInstalled();  
     if (wasInstalled)  
       remove();  
     mfileName = fn;  
     if (!isValid())  
       return;  
     if (wasInstalled)  
       install();  
   }  
   void setLocale(QLocale l)  
   {  
     bool wasInstalled = isInstalled();  
     if (wasInstalled)  
       remove();  
     mlocale = l;  
     if (wasInstalled)  
       install();  
   }  
 public:  
   explicit Translator(QString fn = QString(), QLocale l = QLocale())  
   {  
     minstalled = false;  
     setFileName(fn);  
     setLocale(l);  
   }  
   ~Translator()  
   {  
     remove();  
   }  
 };  
Что это мы тут нагородили? Давайте разбираться. Мы обернули список указателей на QTranslator в удобный для работы класс. Этот класс имеет методы для смены имени файла (например, это appname у файла appname_ru.qm или libname у файла libname_fr.qm, то есть, если можно так выразиться, "значащая" часть имени), а также для смены локали. И, конечно же, мы можем устанавливать и убирать экземпляры нашего класса. При этом внутри используется всё тот же знакомый по документации метод QCoraApplication::installTranslator. Данный класс почти готов к использованию, достаточно только указать, в каких папках искать переводы (метод install, переменная paths).

Уфф, половину проблемы решили. Осталось разобраться с получением списка языков, для которых доступны переводы. Приступим:
 QList<QLocale> availableLocales(QString fileName)  
 {  
   if (fileName.isEmpty())  
     return QList<QLocale>();  
   QList<QLocale> list;  
   QStringList validFiles;  
   QStringList paths; //тут вновь перечисляем пути к папкам, где могут лежать файлы переводов  
   foreach (QString path, paths)  
   {  
     QDir dir(path);  
     QStringList files = dir.entryList(QStringList() << "*.qm", QDir::Files); //получаем список файлов .qm  
     foreach (QString file, files)  
     {  
       //проверяем, что "значимая" часть имени файла соответствует имени, переданному в качестве параметра  
       if (file.left(fileName.length()) == fileName && !validFiles.contains(file))  
         validFiles << file;  
     }  
   }  
   foreach (QString file, validFiles)  
   {  
     int suffixPos = fileName.length() + 1; //индекс, с которого начинается "суффикс" имени файла, содержащий имя локали  
     int suffixLength = validFile.length() - fileName.length() - 4; //длина суффикса: вычитаем длину "значимой" части, длину "_" и длину ".qm"  
     QString localeName = validFile.mid(suffixPos, suffixLength);  
     QLocale l(localeName);  
     list << l;  
   }  
   return list;  
 }  
Вот такая простая, но полезная функция. Тут следует заметить, что при использовании её, а также приведенного выше класса предполагается, что файлы переводов именуются следующим образом: name_xx.qm, где name — "значимая" часть имени, соответствующая названию программы или библиотеки, xx — имя локали, а .qm - стандартный суффикс для файлов переводов, генерируемых средствами Qt.

Однако, пока мы решали проблему удобства, другая, не менее важная проблема — проблема производительности — осталась за кадром. А она имеет место быть. Как известно, перевод интерфейса "на лету" осуществляется путём переопределения метода QWidget::changeEvent, проверки типа события (он должен быть QEvent::LanguageChange) и, собственно, перевода видимых элементов с использованием функции tr. А теперь представьте себе программу, содержащую около тысячи видимых и подлежащих переводу элементов. Пользователь меняет язык интерфейса и все пять (к примеру) файлов перевода сначала убираются, а затем устанавливаются вновь (с другой локалью), вызывая аж десять событий смены языка. Итого — десять тысяч вызовов setText, setToolTip и т.д. Это уже будет заметно на глаз (и едва ли приятно, так как приложение на секунду "подвисает").

Надо что-то делать. Например, можно воспользоваться вот этим примером. Суть его такова: создаётся объект-посредник, к его слоту подсоединяется некий сигнал. При вызове слота запускается два таймера: "промежуточный" (в примере он назван "минимальным", что, на мой взгляд, не совсем корректно) и "максимальный". Если в течение некоторого промежутка времени слот вызывается снова, то "промежуточный" таймер перезапускается. Как только один из таймеров срабатывает, объект испускает соответствующий сигнал. Пример по ссылке выше приспособлен под определённую ситуацию, я же несколько "причесал" его под общий случай. Скачать можно тут.

А теперь то, ради чего было это лирическое отступление. Создадим ещё один простенький класс, который установим в качестве фильтра событий на экземпляр QApplication (или QCoreApplication). Вот он:
 class LanguageChangeNotifier : public QObject  
 {  
   Q_OBJECT  
 private:  
   SignalDelayProxy *proxy;  
 public:  
   explicit LanguageChangeNotifier()  
   {  
     proxy = new SignalDelayProxy(50, 100 this);  
     connect(proxy, SIGNAL(triggered()), this, SIGNAL(languageChanged()));  
   }  
 public:  
   bool eventFilter(QObject *o, QEvent *e)  
   {  
     if (e->type() != QEvent::LanguageChange)  
       return false;  
     proxy->trigger();  
     return true;  
   }  
 signals:  
   void languageChanged();  
 };  
Устанавливаем наш фильтр:
 LanguageChangeNotifier *notifier = new LanguageChangeNotifier;  
 QApplication::installEventFilter(notifier);  
Готово! Теперь в виджетах, где требуется перевод элементов, добавляем слот:
 void OurWidget::retranslateUi()  
 {  
   //тут переводим интерфейс, например так:  
   myLabel->setText(tr("some text"));  
 }  
И где-нибудь в конструкторе, ссылаясь на глобальный экземпляр LanguageChangeNotifier:
 connect(notifier, SIGNAL(languageChanged()), this, SLOT(retranslateUi()));  
Теперь мы получим крохотную задержку в одну десятую секунды (которую едва ли можно заметить) перед сменой языка интерфейса, зато избавимся от потенциально гораздо более существенной задержки, а также от зависания приложения из-за десяти тысяч лишних вызовов функций (от которых мы, к слову, тоже избавимся).

четверг, 4 июля 2013 г.

Пароли: безопасный ввод и хранение

Перейду сразу к рассмотрению примера. Допустим, у нас есть некий сервер, к которому подключаются пользователи. Для идентификации используются логин и пароль. Клиентами могут быть как консольные приложения, так и приложения с графическим интерфейсом. Задача: организовать безопасный ввод пароля, а также не менее безопасные хранение (как на сервере, так и в настройках клиента) и передачу этого пароля на сервер. Что ж, приступим.

Начнём с хранения на сервере и передачи. Тут всё достаточно просто: пароль нужно хранить и передавать в зашифрованном виде. Вполне подойдёт, например, алгоритм SHA-1. В Qt есть замечательный класс QCryptographicHash, позволяющий зашифровать пароль при помощи выбранного алгоритма, в том числе и SHA-1. Как это выглядит на практике:
 QString pwd;  
 //вводим пароль  
 QByteArray encryptedPwd = QCryptographicHash::hash(pwd.toUtf8(), QCryptographicHash::Sha1);  
Готово, пароль зашифрован. В таком виде мы его и будем передавать на сервер (скажем, при помощи QTcpSocket) и хранить там, например, в базе данных:
 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");  
 //задаем нужные параметры, такие как имя файла или адрес сервера  
 QSqlQuery q(db);  
 q.prepare("INSERT INTO users (login, password) VALUES (:login, :password)");  
 //биндим логин и прочие необходимые данные пользователя  
 q.bindValue(":password", encryptedPwd); //биндим наш пароль  
 q.exec();  
Теперь в нашей базе хранится пароль в зашифрованном виде. О базах данных и о работе с сетью в Qt много и доходчиво написано в официальной документации, не буду заниматься дублированием.

Поговорим о вводе пароля и его хранении в настройках клиента (вряд ли кому-то приятно каждый раз при открытии приложения заново вводить пароль). Тут в целом всё довольно просто. Используем QSettings:
 QSettings s;  
 s.setValue("password", encryptedPassword);  
Вот и всё. Но есть один нюанс, о котором будет сказано дальше. Пока же перейдём к вводу.

Начнём, пожалуй, с ввода из консоли (или из терминала, кому как больше нравится). Казалось бы, делаем std::cin >> pwd и радуемся жизни, но не тут то было. Если вы хоть раз пользовались консольными приложениями, требующими ввода пароля, то скорее всего помните, что при его вводе символы, которые вы печатали, не отображались в окне. Сделано это, как нетрудно догадаться, для того, чтобы никто не подсмотрел пароль. Попробуем сделать так же? Попробуем:
 #include <cstdio>  
   
 #if defined(Q_OS_LINUX) || defined(Q_OS_MAC)  
 #include "termios.h"  
 #elif defined(Q_OS_WIN)  
 #include "windows.h"  
 #endif  
   
 void setStdinEchoEnabled(bool enabled)  
 {  
 #if defined(Q_OS_MAC) || defined(Q_OS_LINUX)  
   struct termios tty;  
   tcgetattr(STDIN_FILENO, &tty);  
   if(enabled)  
     tty.c_lflag |= ECHO;  
   else  
     tty.c_lflag &= ~ECHO;  
   tcsetattr(STDIN_FILENO, TCSANOW, &tty);  
 #elif defined(Q_OS_WIN)  
   HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);  
   DWORD mode;  
   GetConsoleMode(hStdin, &mode);  
   if(enabled)  
     mode |= ENABLE_ECHO_INPUT;  
   else  
     mode &= ~ENABLE_ECHO_INPUT;  
   SetConsoleMode(hStdin, mode);  
 #endif  
 }  
Выглядит достаточно монструозно, давайте немного разберёмся. Назначение функции довольно простое: она включает или отключает отображение вводимых в консоль символов. Директивы #define нужны потому, что на разных платформах это делается по-разному (и подключаемые заголовки тоже разные). Подробнее об использованных структурах и функциях можно почитать в интернете, а пока посмотрим, как это использовать на практике:
 std::cout << "Enter password: ";  
 setStdinEchoEnabled(false);  
 std::string pwd;  
 std::cin >> pwd;  
 setStdinEchoEnabled(true);  
 std::cout << "\n";  
Последняя строчка нужна для того, чтобы ввести "съеденный" при вводе пароля с отключённым "эхом" символ конца строки.

Чтобы не возиться с std::cin и std::cout, можно ввести следующие функции:
 void write(const QString &s)  
 {  
   static QTextStream out(stdout, QIODevice::WriteOnly);  
   out << s;  
   out.flush();  
 }  
   
 QString readLine()  
 {  
   static QTextStream in(stdin, QIODevice::ReadOnly);  
   return in.readLine();  
 }  
Подробнее о QTextStream и его использовании написано в документации. Теперь ввод пароля может выглядеть так:
 write("Enter password: ");  
 setStdinEchoEnabled(false);  
 QString pwd = readLine();  
 setStdinEchoEnabled(true);  
 write("\n");  
На мой взгляд, это чуточку проще и понятнее, а также избавляет от необходимости производить преобразования между типами стандартной библиотеки C++ и типами Qt.

А теперь перейдём к последнему и самому, как мне кажется, интересному — вводу пароля с использованием графического интерфейса. За основу возьмём QLineEdit. Создадим следующий класс:
 class PasswordWidget : public QWidget  
 {  
   Q_OBJECT  
 private:  
   QByteArray encPwd;  
   QLineEdit *ledt;  
 public:  
   explicit PasswordWidget(QWidget *parent = 0)  
   {  
     QHBoxLayout *hlt = new QHBoxLayout(this);  
     hlt->setContentsMargins(0, 0, 0, 0);  
     ledt = new QLineEdit;  
     ledt->setEchoMode(QLineEdit::Password);  
     connect(ledt, SIGNAL(textChanged(QString)), this, SIGNAL(passwordChanged()));  
     hlt->addWidget(ledt);  
   }  
 public:  
   void setPassword(const QString &pwd)  
   {  
     encPwd.clear();  
     ledt->setPlaceholderText("");  
     ledt->setText(pwd);  
   }  
   void setEncryptedPassword(const QByteArray &pwd, int charCountHint = 0)  
   {  
     encPwd = pwd;  
     ledt->clear();  
     ledt->setPlaceholderText(QString().fill('*', (charCountHint > 0) ? charCountHint : 0));  
   }  
   void clear()  
   {  
     encPwd.clear();  
     ledt->setPlaceholderText("");  
     ledt->clear();  
   }  
   void restoreState(const QByteArray &ba)  
   {  
     QDataStream in(ba);  
     bool enc = false;  
     in >> enc;  
     if (enc)  
     {  
       QByteArray pwd;  
       int cc = 0;  
       in >> pwd;  
       in >> cc;  
       setEncryptedPassword(pwd, cc);  
     }  
     else  
     {  
       QString pwd;  
       in >> pwd;  
       setPassword(pwd);  
     }  
   }  
   QString password() const  
   {  
     return ledt->text();  
   }  
   QByteArray encryptedPassword(QCryptographicHash::Algorithm a) const  
   {  
     if (!ledt->text().isEmpty())  
       return QCryptographicHash::hash(ledt->text().toUtf8(), a);  
     else if (!encPwd.isEmpty())  
       return encPwd;  
     else  
       return QByteArray();  
   }  
   int сharCountHint() const  
   {  
     return ledt->placeholderText().length();  
   }  
   QByteArray saveState() const  
   {  
     QByteArray ba;  
     QDataStream out(&ba, QIODevice::WriteOnly);  
     out << false;  
     out << password();  
     return ba;  
   }  
   QByteArray saveStateEncrypted(QCryptographicHash::Algorithm method) const  
   {  
     QByteArray ba;  
     QDataStream out(&ba, QIODevice::WriteOnly);  
     out << true;  
     out << encryptedPassword(a);  
     out << charCountHint();  
     return ba;  
   }  
 signals:  
   void passwordChanged();  
 };  
Посмотрим, что тут к чему. Во-первых, мы инкапсулировали QLineEdit внутри нашего класса, чтобы к нему не было прямого доступа (например, чтобы нельзя было менять текст). Далее, мы сделали так, чтобы вместо реальных символов всегда отображались звёздочки. Пароль при этом можно задавать не только в явном ("как есть"), но и в зашифрованном виде. Во втором случае можно также указать, из скольких символов состоит пароль, чтобы отображалось именно столько звёздочек (charCountHint). Сигнал passwordChanged() оповещает о том, что пароль был каким-либо образом изменён (программно, либо при вводе или удалении символов пользователем).

Теперь о главном — о хранении. Метод saveStateEncrypted() сериализует параметры пароля в массив байтов. Указывается тип пароля (зашифрованный или явный), а также собственно пароль и, в случае, если он зашифрован, подсказка о количестве символов. Десериализация происходит с точностью до наоборот. Посмотрим, как это применить на практике:
 //создание виджета  
 PasswordWidget *pwgt = new PasswordWidget;  
   
 //сохранение пароля при закрытии приложения  
 QSettings s;  
 s.setValue("password", pwgt->saveStateEncrypted(QCryptographicHash::Sha1));  
   
 //восстановление пароля при открытии приложения  
 QSettings s;  
 pwgt->restoreState(s.value("password").toByteArray());  
Вот, собственно, и всё. Для большинства приложений, где не требуются крайние меры по защите информации, приведённых способов работы с паролями должно хватить. Имеющиеся функции и класс PasswordWidget, конечно же, могут быть улучшены, дополнены разными возможностями, но это уже, как говорится, совсем другая история.

среда, 3 июля 2013 г.

Сериализация и легко расширяемые протоколы в Qt

Во время работы над одним проектом, где обмен данными по сети является ключевым моментом, у меня возникла идея поделиться своими соображениями насчет проблем сериализации и расширения протоколов связи клиент-серверных приложений.

Сразу скажу, что эта заметка в первую очередь ориентирована на сериализацию при передаче данных по сети и едва ли пригодится в приложениях, где ведётся работа с большими объёмами данных, когда каждый байт на счету и оптимизация превыше всего. Если же предполагается, что храниться или передаваться будут небольшие порции данных, то изложенное ниже может быть вам полезно. Впрочем, гениальных решений и "срыва покровов" ждать не стоит.

Итак, сериализация в Qt. Обычно для этой цели используется класс QDataStream. Документация, на мой взгляд, достаточно полно освещает все аспекты его применения, поэтому ограничусь кратким обзором. QDataStream сериализует объекты (тут и далее к объектам я буду причислять также данные таких типов как int, bool и т.д., хотя это и не совсем корректно) разных типов в поток данных, что понятно из его названия. При этом данные могут как добавляться к QByteArray, так и записываться сразу в QIODevice (например, QFile или QTcpSocket). Естественно, присутствует возможность десериализовать объекты обратно из потока данных.

Казалось бы, все замечательно, пишем данные в поток, забираем обратно, тишь да гладь, да божья благодать. Но вот нам потребовалось расширить протокол, добавив новый объект. И тут начинается хаос. Для наглядности предположим, что у нас есть следующий класс:
 class Book  
 {  
 public:  
   QString title;  
   QString text;  
 public:  
   friend QDataStream &operator <<(QDataStream &stream, Book book)  
   {  
     stream << book.title;  
     stream << book.text;  
     return stream;  
   }  
   friend QDataStream &operator >>(QDataStream &stream, Book &book)  
   {  
     stream >> book.title;  
     stream >> book.text;  
     return stream;  
   }  
 };  
Тут мы добавили дружественные операторы, которые позволяют нам делать следующее:
 //на стороне отправителя  
 QTcpSocket *s = new QTcpSocket;  
 //подключение и т.д.  
 QDataStream out(s);  
 for (int i = 0; i < 10; ++i)  
 {  
   Book b;  
   //задаем какие-то значения b.title и b.text  
   out << b; //сериализуем наш объект  
 }  
   
 //на стороне получателя  
 QDataStream in(s);  
 for (int i = 0; i < 10; ++i)  
 {  
   Book b;  
   out >> b; //десериализуем наш объект  
 }  
А теперь мы вдруг вспомнили, что у книги есть ещё и автор:
 class Book  
 {  
 public:  
   QString title;  
   QString text;  
   QString author; //новая переменная класса  
 };  
Добавили переменную, модифицируем оператор сериализации:
 friend QDataStream &operator <<(QDataStream &stream, Book book)  
 {  
   stream << book.title;  
   stream << book.text;  
   stream << book.author;  
   return stream;  
 }  
Ура, мы сломали совместимость! Вот просто так взяли, и сломали. Если получатель не обновил программу, то первый объект будет десериализован нормально, у второго же вместо названия окажется автор первого, а вместо текста - название первого. Чтобы убедиться воочию, попробуйте собрать и запустить пример.

Как же быть? Документация предлагает нам использовать версионирование. Не буду заниматься повторением, однако замечу, что такой подход рано или поздно приведёт к конструкциям вида:
 switch (version)  
 {  
 case 1:  
   //50 строк  
   break;  
 case 2:  
   //55 строк  
   break;  
 case 3:  
   //56 строк  
   break;  
 //ещё около 30 версий  
 }  
Думаю, не надо говорить о поддерживаемости подобного кода (проще застрелиться, чем заниматься этим).

Но что же тогда делать? Выход есть. Не самый оптимальный с точки зрения трафика/места на диске, но идеальный с точки зрения удобства. Воспользуемся замечательным классом QVariantMap (если всё же хочется сэкономить хотя бы на ключах, используем QMap<quint16, QVariant> или даже QMap<quint8, QVariant>). Волшебным образом, неподдерживаемый код превращается в...
 friend QDataStream &operator <<(QDataStream &stream, Book book)  
 {  
   QVariantMap m;  
   m.insert("title", book.title);  
   m.insert("text", book.text);  
   m.insert("author", book.author);  
   m.insert("publisher", book.publisher);  
   //сколько угодно добавляем или удаляем сериализуемые переменные  
   stream << m;  
   return stream;  
 }  
Да, вот в такой удобный, полностью совместимый код. Кто не знаком с шаблоном QMap, поясню: это так называемый словарь, где каждому ключу (идентификатору) соответствует некоторое значение. При попытке обратиться к несуществующему значению возвращается объект этого типа, для создания которого используется конструктор по умолчанию. В нашем случае используется QVariantMap, то есть в качестве типа ключей выступает QString, а в качестве типа значений - QVariant (который может содержать любой зарегистрированный тип данных).

Аналогично поступаем с оператором десериализации:
 friend QDataStream &operator >>(QDataStream &stream, Book &book)  
 {  
   QVariantMap m;  
   stream >> m;  
   book.title = m.value("title").toString();  
   book.text = m.value("text").toString();  
   book.author = m.value("author").toString();  
   book.year = m.value("year").toUInt(); //новая переменная  
   //сколько угодно добавляем или удаляем переменные  
   return stream;  
 }  
В данном случае book.year будет присвоено значение для unsigned integer по умолчанию (то есть 0). А имеющееся значение publisher просто будет проигнорировано. При этом все остальные книги также будут десериализованы корректно.

Кончено, такой протокол является несколько избыточным, ведь даже если получатель никак не использует часть данных, они всё равно ему передаются. Кроме того, размер передаваемых данных увеличивается из-за наличия ключей и из-за прослойки в виде QMap. Если требуется передавать много объектов с большим числом полей, каждое из которых имеет небольшой размер, то издержки приведённого подхода будут особенно заметны. Однако я ещё не встречал более простого в поддержке способа сохранить совместимость при независимой модификации сервера и клиента.

понедельник, 24 июня 2013 г.

Собираем Qt5 в Ubuntu 10.04

Да-да, вы не ошиблись. Именно 10.04. "Но как, почему, зачем такое извращение?!" — спросите вы, — "Ведь есть 12.04, 12.10, 13.04, наконец!" Что ж, если вас устраивают падения центра приложений, исчезновение окон и прочие маленькие радости уже на третий день аптайма, — дело ваше. Меня не устроили, и я решил остаться на проверенной, надёжной 10.04. Однако время не стоит на месте, вышел Qt5, который просто так установить и использовать на старенькой системе не получилось. Пришлось засучить рукава и начать поиск решения. Вообще, со всем этим связана целая история (весьма печальная, надо сказать), но о ней я напишу как-нибудь в другой раз. А теперь заканчиваю разглагольствования и перехожу к сути.

Итак, предположим, что у нас есть чистая Ubuntu 10.04. Впрочем, "не очень чистая" и даже совсем "грязная" тоже подойдут. Прежде всего, необходимо установить некоторые пакеты, если они у вас еще не установлены, а также скачать необходимые исходники (да, придется заниматься этим страшным делом — собирать библиотеки из исходников, поскольку не все они доступны в виде готовых deb-пакетов).

Убедитесь, что включены все источники приложений (Система, Параметры, Источники приложений). Чтобы установить нужные пакеты, просто наберите в терминале:

sudo aptitude install m4 gcc g++ make libxcb1 libxcb1-dev libx11-xcb1 libx11-xcb-dev libxcb-keysyms1 libxcb-keysyms1-dev libxcb-image0 libxcb-image0-dev libxcb-shm0 libxcb-shm0-dev libxcb-icccm1 libxcb-icccm1-dev libxcb-sync0 libxcb-sync0-dev libxcb-render-util0 libxcb-render-util0-dev libxcb-xfixes0-dev libxrender-dev libxcb-shape0-dev libxcb-randr0-dev libxcb-glx0-dev libxext-dev libicu-dev

Если планируется собирать модуль WebKit, то к списку следует добавить следующие пакеты:

bison flex gperf ruby

Кроме того, при сборке WebKit потребуется пропатчить исходники, но об этом далее.

Чтобы можно было использовать новый стандарт C++11, нам потребуется собрать новую версию GCC (та версия, что имеется в репозиториях, давно устарела). Если использовать C++11 не планируется, пропускаем следующий шаг.

Качаем исходники. Прежде всего нам потребуется GCC версии 4.7. Надо сказать, что сам я не пробовал собирать Qt5 с GCC версии меньше 4.7, но, если верить вот этой теме на форуме Qt, то версии 4.6 будет достаточно. Версия 4.8, если опять же верить другой теме, не поддерживается. Возможно, в следующих патчах это будет исправлено. Итак, качаем GCC 4.7. Выбираем зеркало и ищем нужный релиз, либо (если очень лень) клацаем по ссылке.
Теперь нужно скачать библиотеки GMP, MPFR и MPC (нет, это не известный медиаплеер, а библиотека multiprecision). Найти исходники можно на официальных сайтах, либо на FTP, там же, откуда качали GCC, только в поддиректории infrastructure. Ленивые клацают по ссылкам:

Но и это ещё не всё. Идём по ссылке и качаем исходники последней версии Qt5.


Рекомендуется скачать именно самую последнюю версию, но, если голос лени всё же сильнее голоса разума, то клацаем сюда (это не самый последний релиз, поэтому ещё раз подчеркну, что лучше скачать более новый вручную).

Также рекомендую сразу скачать последнюю версию QtCreator, если вы ещё её не скачали. В моем случае это была версия 2.7.1.

Если планируется использовать модуль WebKit, то необходимо запастись патчем для исходников (хоть это и несколько громкое название). Дело в том, что во время сборки WebKit генерируется некоторое количество файлов. Однако, по неизвестной причине на Ubuntu 10.04 некоторые файлы не создаются, что приводит к ошибке во время сборки. Я изрядно поломал голову над решением этой проблемы, однако выход оказался куда как прост: достаточно собрать Qt5 в системе, где ошибок не возникает (в той же Ubuntu 12.04), а затем скопировать недостающие файлы в папку с исходниками на "проблемной" системе. Впрочем, нет необходимости делать это снова, просто качайте архив с патчем.

Вот теперь с закачками покончено. Но основное "веселье" только начинается. Распаковываем всё в какую-нибудь папку (у меня это была /home/user/tmp). Лучше, чтобы в пути не было символов пробела (да, на дворе 2013 год, а make и другие утилиты всё ещё не очень хорошо дружат с пробелами).

Начнём со вспомогательных библиотек (GMP, MPFR и MPC). По умолчанию они устанавливаются в /usr/local (сами бинарные файлы, естественно, в /usr/local/lib). Обычно менять путь не требуется, поэтому будем рассматривать вариант по умолчанию. Самые смелые и опытные могут поэкспериментировать.

Переходим в папку с GMP и набираем:

./configure && make && sudo make install

Можно после install добавить параметр -jN, где N — количество потоков (обычно оптимальным числом потоков является количество ядер процессора).

Дожидаемся, пока закончится сборка, вводим пароль суперпользователя, ждём ещё немного, пока закончится установка, и переходим в папку с MPFR. Повторяем процедуру, переходим в папку с MPC и делаем всё то же самое.

Далее, выполняем команду:

export LD_LIBRARY_PATH=/usr/local/lib

Это нужно для того, чтобы указать системе путь к новым библиотекам. Важно: данная команда действует только для того окна терминала, в котором была выполнена, либо для того скрипта, в котором была выполнена. Поэтому не закрываем окно терминала и все последующие команды выполняем в нём.

Теперь переходим в папку с GCC и выполняем:

./configure --with-gmp=/usr/local/lib --with-mpfr=/usr/local/lib --with-mpc=/usr/local/lib && make && sudo make install

Параметры --with-* нужны, чтобы указать путь к соответствующим библиотекам, так как они установлены по нестандартному пути (обычно библиотеки лежат в /usr/lib). Если при сборке всё же появится сообщение о том, что мол не найдена библиотека такая-то (у меня это была MPC, так как я случайно закрыл терминал и забыл ввести команду export), то выполняем:

sudo cp /usr/local/lib/libmpc.so.2 /usr/lib

Вместо libmpc вписываем нужную библиотеку. После этого перезапускаем сборку.

GCC компилируется достаточно долго, можно пойти попить чаю. Однако, все когда-нибудь заканчивается, и после завершения сборки и установки GCC мы переходим в папку с Qt5.

Если требуется модуль WebKit, то копируем папку qtwebkit-examples-and-demos из патча в папку с Qt5. Подтверждаем слияние папок и замену файлов. В папке qtwebkit-examples-and-demos содержится только лишь один файл, препятствующий сборке примеров для этого модуля. Дело в том, что у меня при сборке примеров возникла очередная ошибка (компилятор не мог найти точку входа в какую-то процедуру). Поскольку примеры мне не были нужны, я просто исключил их из процесса сборки. Тем не менее, если у кого-то есть желание потратить вечерок-другой на решение проблемы — можно просто не копировать папку qtwebkit-examples-and-demos. Но я бы не советовал... (Нет, правда, оно вам надо?)

Теперь выполняем следующую команду:

./configure -prefix /home/user/qt/5.0.2 -opensource -fully-process -qt-sql-sqlite -no-qml-debug -qt-xcb -nomake examples -nomake demos -nomake tests -nomake docs -skip webkit

Поясним, что означают параметры configure:
  • -prefix /home/user/qt/5.0.2 Задаёт путь, куда будут установлены скомпилированные библиотеки Qt. Можно заменить на любой другой (только без пробелов).
  • -opensource Указывает, под какой лицензией планируется использовать Qt. Можно пропустить, тогда выбрать лицензию будет предложено в процессе сборки.
  • -fully-process Также необязательный параметр. Указывает, что следует создавать Makefile для каждого подпроекта.
  • -qt-sql-sqlite Снова необязательный параметр. Позволяет встроить плагины SQL прямо в соответствующий модуль (QtSql). В данном случае будет встроен плагин SQLITE.
  • -no-qml-debug Необязательный параметр. Не указывайте его, если планируете пользоваться дебаггером для QML.
  • -qt-xcb Этот параметр не является обязательным, но при сборке на Ubuntu 10.04 лучше его указать, так как, если верить различным источникам в интернете, без него могут быть ошибки при сборке (видимо, это связано с отсутствием каких-то библиотек или заголовочных файлов).
  • -nomake examples и -nomake demos Эти параметры указывают, что не нужно собирать примеры приложений. Опустите их, если вам требуется собрать примеры.
  • -nomake tests А этот параметр указывает, что не нужно собирать тесты (в самом деле, если мы не собираемся всерьёз заниматься тестированием Qt, нам они ни к чему).
  • -nomake docs Отказ от сборки документации. С одной стороны, документация идёт в комплекте с QtCreator, а потому собирать ее отдельно нет смысла, с другой — этот процесс занимает так мало времени, что уже успел бы завершиться, пока читаются эти малополезные лирические отступления.
  • -skip webkit Пропускает указанный модуль (он не будет собран). Можно указать также gui, widgets, sql и т.д. Если использовать WebKit не планируется, то данный параметр поможет сэкономить весьма и весьма приличное количество времени (я не замерял специально, но субъективно WebKit собирается дольше чем все остальные модули вместе взятые).
Сразу после запуска configure будет предложено подтвердить принятие лицензионного соглашения. Вводим yes и нажимаем Enter.

Опять же, если было решено собрать модуль WebKit, то копируем папку qtwebkit  из патча в папку с Qt5. Подтверждаем слияние папок и замену файлов. В папке qtwebkit содержатся необходимые исходники, сгенерированные на другой, "беспроблемной" системе.

Далее выполняем:

make && make install

Важно: в данном случае не следует указывать параметр -jN, т.к. это приводит к неожиданным ошибкам, а сборку приходится запускать ещё раз. Почему? Потому что разработчик, видимо, не учёл возможность параллельной компиляции.

Qt5 собирается ещё дольше, чем GCC, поэтому можете смело пойти на получасовую прогулку, или сделать небольшую уборку, пока он компилируется.

После того, как библиотеки Qt наконец собрались, добавляем путь к ним в QtCreator. Те, кто знает, как это делается, и те, кто не пользуется QtCreator, могут дальше не читать.

Итак, открываем QtCreator и выбираем пункт Параметры... меню Инструменты.


Переходим на вкладку Компиляторы, добавляем наш компилятор в /usr/local, жмём Применить... Затем во вкладке Профили Qt жмём кнопку Добавить... и указываем путь к исполняемому фалу qmake в той папке, куда установили библиотеки Qt5.


Задаём какое-нибудь легко узнаваемое название профиля, жмём Применить... и переходим ко вкладке Комплекты, создаём новый, опять же присваиваем имя, выбираем только что созданный профиль Qt и компилятор.


Жмём ОК и — готово!