воскресенье, 26 декабря 2010 г.

Qyoto and GC issues

Сегодня пол дня слушал песню Amy Winehouse "You Know I'm No Good" и долбался с одной проблемой в Qyoto (а их по-видимому там не мало). До причины я все-таки докопался, но хотел описать все завтра, так как сейчас уже довольно поздно, но неожиданно для себя в одном из источников по Qyoto, с которым я работал, в одном из примеров обнаружил метку, лежащую на форме с цитатой из этой песни. Это судьба, это словно зуб Гоя (Серьезный Человек), подумал я и решел все-таки написать пост сегодня.
А проблема следующая: У меня есть два идентичных куска кода на C# (Qyoto) и на C++ (Qt 4.7), создающие QMainWindow с QTreeView, лежащем на нем. Этому TreeView скармливается QStandardItemModel содержащая в себе некоторые QStandardItem'ы. В случае Qt все работает хорошо, а вот в Qyoto item'ы на мгновение появляются, а потом почему-то исчезают.
В результате выяснилось, что item'ы на которые нет ссылок из managed кода просто собираются GC, не смотря на то, что они были добавлены в модель и по идее ссылки на них должны храниться в этой самой QStandardItemModel!!! Такова уж специфика работы Smoke, будем иметь это ввиду.
Выяснить это удалось при помощи слабых ссылок. Ниже приведен код на примере которого можно убедиться в верности предположения:

using System;
using Qyoto;
using System.Collections.Generic;
namespace QyotoDrv
{
  public class MainWindow : QMainWindow
  {
    Ui.MainWindow _ui;
    
    QStandardItem it1; //menu item referred from MainWindow

    WeakReference _wr1; //weak reference to it1
    WeakReference _wr2; //weak reference to it2
    
    const string SlotShowWR = "slot_ShowWR()";
    
    public MainWindow ()
    {
      _ui = new Ui.MainWindow(); 
      _ui.SetupUi(this);  
      
      SetModel();
    }
    
    void SetModel()
    {
      var model = new QStandardItemModel(_ui.treeView);
      
      it1 = new QStandardItem("Hello, World!");
      model.AppendRow(it1);

      //the following menu item is not referred from MainWindow and will be collected by GC,
      //in spite of being added to the model!!!
      QStandardItem it2 = new QStandardItem("Good bye, World!");
      model.AppendRow(it2);
      
      _ui.treeView.SetModel(model);

      //set weak references pointing to the corresponding menu items
      _wr1 = new WeakReference(it1);
      _wr2 = new WeakReference(it2);
      
            
      QMenu file = MenuBar().AddMenu("Click me!");
      
      QAction showWR = new QAction("Show Weak References", this);
      file.AddAction(showWR);
      Connect(showWR, SIGNAL("triggered()"), this, SLOT(SlotShowWR));
    }
    
    [Q_SLOT(SlotShowWR)]
    void ShowWeakReference()
    {
      //after the main window will be shown wait a fiew seconds until the second item has disappeared
      Console.WriteLine("wr1.IsAlive={0}", _wr1.IsAlive); //is alive
      Console.WriteLine("wr2.IsAlive={0}", _wr2.IsAlive); //is not alive!
    }
  } 
}


* This source code was highlighted with Source Code Highlighter.


Запустите приложение. После появления главного окна дождитесь пока второй item не исчезнет, после чего щелкните на пункт меню "Click me! > Show Weak References" и посмотрите на консоль.
Комменты на английском сделаны для возможных англоязычных посетителей, чтоб хоть чем-то помочь им, слишком уж мало материала в сети на тему Qyoto.

понедельник, 20 декабря 2010 г.

UI-калка

Для удобства можно включать .ui фалы в C#-проект настроив pre-build step так, чтобы они автоматически ui-кались, т.е. утилита uics генерировала из них .cs файлы (см. предыдущий пост). Тогда вы сможете редактировать их в QtDesigner просто кликнув по ним в Solution Explorer не выходя из MonoDevelop.IDE автоматически проставит для них свойства "Build Action=Nothing" и "Copy to output directory=Do not copy" (на всякий случай проконтролируйте это), а значит они не будут встраиваться в результирующую сборку в качестве "embedded" ресурсов и их существование в проекте - вопрос исключительно удобства.
Для того что-бы .ui файлы автоматически ui-кались при компиляции (точнее перед ее началом) создадим скрипт uicAll.sh и положим его в папку проекта. Содержимое скрипта следующее:

for uifile in `find $1 -name "*.ui"`
do
    csfile="${uifile%.*}.Designer.cs"
    if [ ! -f $csfile ] || [ `stat -c %Y $uifile` -gt `stat -c %Y $csfile` ]; then
        echo "uicking... $uifile"
        uics "$uifile" > "$csfile"
    fi
done


После чего добавим в свойствах проекта новый pre-build step как на скриншоте ниже:

пятница, 17 декабря 2010 г.

Qyoto

Не то, что бы я любитель экзотики, но так получилось, что у меня дома теперь Fedora 14 и MonoDevelop. Хочется писать кросс-платформенные приложения с удобным и красивым GUI, но к сожалению WPF на mono не портирован и видимо портирован никогда не будет. Windows Forms выступать альтернативой WPF просто не может, а GTK# мне не интересен, возможно потому, что напоминает мне Windows Forms.
К счастью я несколько знаком с Qt и знаю насколько мощным является этот Framework и какие красивые и дружественные пользователю GUI он позволяет создавать. Кроме того, оказалось что для Qt существует binding под mono, называемый Qyoto, а возможность работать с QT напрямую из .Net кажется весьма привлекательной. Недостатком Qyoto является малое количество документации к нему.

Для начала работы вам понадобится установить следующие пакеты (для Fedora KPackageKit)
* qyoto - сами библиотеки, основная из них (qt-dotnet.dll)
* qyoto-devel - утилиты для разработки, рекомендую установить, если вы собираетесь работать с ui файлами
* qt-devel - если вы хотите работать с QtDesigner (это нативный Qt-шный дезайнер)

В сети достаточно материалов о том как создавать простые приложения и как связывать сигналы и слоты.
Ниже приведено несколько полезных ссылок:
http://zetcode.com/tutorials/qyotosharptutorial/
http://ibboard.co.uk/Programming/using-qyoto.html
http://www.kdedevelopers.org/node/2090

Во всех примерах GUI создается прямиком из кода, мне не удалось найти ниодного описания того, как подключать .ui файлы созданные в QtDesigner (MonoDevelop, не имеет дизайнера для qyoto).
И все же такая возможность существует! В пакет qyoto-devel входит утилита uics предназначенная именно для этого.
Предположим, что у вас имеется файл MainWindow.cs с классом MainWindow : QMainWindow, который вы хотите инициализировать из файла MainWindow.ui. Для этого, при помощи утилиты uics вам сначала придется сгенерировать .cs файл, некий аналог .Designer.cs файла в Windows Forms:

$ uics MainWindow.ui > MainWindow.Designer.cs

В выходном файле будет два класса: Ui_MainWindow и Ui.MainWindow.
Вам нужно будет в конструкторе MainWindow создать экземпляр Ui.MainWindow класса, и при помощи метода SetupUi этого обьекта инициализировать MainWindow.
Все элементы GUI являются public членами класса Ui.MainWindow, поэтому для того, чтоб всегда иметь возможность доступа к ним, экземпляр класса Ui.MainWindow сохраняется в соответствующем поле класса MainWindow, как показано в приведенном ниже примере:

class MainWindow : QMainWindow
{
  Ui.MainWindow _ui;

  public MainWindow ()
  {
    _ui = new Ui.MainWindow();
    _ui.SetupUi(this);
    
    //инициализация прочих членов
    
    Show();
    
  }

  /* Члены класса */
}


* This source code was highlighted with Source Code Highlighter.


Qt-шные классы (контролы и прочее) описаны в библиотеке qt-dotnet.dll. Эта библиотека должна быть подключена к вашему проекту.
В общем Qyoto кажется вполне пригодным к применению. Был правда случай когда регистр первой буквы свойства сгенеренного uics отличался от регистра первой буквы этого же свойства в qt-dotnet.dll, в результате чего сгенерированный код оказался некомпилируемым. Я столкнулся с такой ситуацией пока только для одного свойства отдельно взятого класса. В моем случае можно было легко отказаться от использования этого класса, что я и сделал, но думаю, что проблему можно было бы решить и иначе: например поправив исходники uics. Пока, что это не кажется проблемой. Посмотрим, что будет дальше!

VS Debugger

По работе мне часто приходится отлаживать mixed код в Visual Studio 2008. Порой студию клинит и отладчик перестает входить в некоторые функции и останавливаться на установленных в них бряках. Поскольку случается это не очень часто, то способы лечения этой проблемы быстро забываются и каждый следующий раз оказывается как первый. Чтоб облегчить себе жизнь в будущем и, возможно, немного помочь другим, я опишу в этом посте все необходимые шаги, пока информация еще свежа после последней терапии. Ниже использованы скриншоты из VS 2010, но для VS 2008 действия абсолютно аналогичны.

1. Если вы используете mixed код, то в свойствах всех проектов вам придется установить тип отладчика "mixed".

В C++ проектах:



В C# проектах:



Далее в свойствах компилятора указываем формат отладочной информации. Нам нужно, чтоб она хранилась в pdb (Program DataBase) файле, поэтому выбираем опцию "Program Database /Zi" или следующую за ней (честно говоря второе я никогда не пробовал), как на следующем скриншоте.



Теперь осталось только указать компановщику, чтоб он генерил .pdb базу:



Все остальные проблемы решаются подчисткой ncb, suo, obj и т.п. файлов и полной перекомпиляцией, но это уже по мере необходимости.

Во время отладки вы можете проверить загружена ли отладочная информация для нужных вам модулей следующим образом: При запущенном отладчике в студии нужно войти в пункт меню Debug > Windows > Modules.



В открывшемся списке вы сможете найти инересующий вас модуль.Если в колонке "Symbol File" вы увидите имя вашего pdb, а в колонке "Symbol Status" рядом с ним - статус "Symbols loaded", то все в порядке, в противном случае вы можете попытаться загрузить его вручную из соответствующего пункта контекстного меню как это показано на скриншоте ниже:



Это, пожалуй все.
Вот ссылка на пост, где я нашел часть приведенной выше информации: http://geekswithblogs.net/dbutscher/archive/2007/06/26/113472.aspx

четверг, 16 декабря 2010 г.

yyyy.MM.dd HH.mm.ss -> time_t

Поскольку time_t это время (UTC) в секундах, прошедшее с 1 января 1970 года, 00:00:00, то переход от System.DateTime к time_t легко осуществить следующим образом:

DateTime time = DateTime.UtcNow; //предположим что это вермя которое мы хотим сконвертить к time_t
DateTime unixEpochMidnight = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long time_t = (long)(time - unixEpochMidnight).TotalSeconds;


* This source code was highlighted with Source Code Highlighter.


Однако недавно передо мной стала задача в том, чтобы разобрать строчку с простым представлением даты и времени, что-то типа "2010-12-15 14:30:00", сконвертировать ее в time_t, но сделать все это нужно было в unmanaged коде на C++. В том чтобы распарсить строку нет ничего сложного. Проблема только в том, чтобы из года, месяца, дня, часа, минут, секунд перейти к time_t. Сложность встает благодаря наличию високосных годов, однако она оказалась довольно просто решаемой.
Ниже приведен C# код программы осуществляющей преобразование к time_t. Почему не C++? Потому, что мне было удобней сначало решить задачку на языке который я лучше знаю, а потом портировать решение на С++

using System;

namespace TimeCs
{

  class Program
  {

    const long SecPerDay = 3600 * 24;
    const long A1 = 365 * SecPerDay; //=365 * 86400
    const long A2 = SecPerDay / 4; //=21600
    const long A3 = -SecPerDay / 100; //=864 Обратите внимание на занк "-"
    const long A4 = SecPerDay / 400; //=216
    const long SecPerYear = A1 + A2 + A3 + A4;


    static long SubYearsToSec(int y1, int y2)
    {
      long dif1 = SecPerYear * y1 / SecPerDay;
      long dif2 = SecPerYear * y2 / SecPerDay;
      return (dif1 - dif2) * SecPerDay;
    }
    

    static bool IsLeapYear(int year)
    {
      if (year % 400 == 0)
        return true;

      if (year % 100 == 0)
        return false;

      return year % 4 == 0;       
    }
    

    //month 1..12
    //day 1..31
    static int ToDayInYear(int year, int month, int day)
    { 
      int daysInFeb = IsLeapYear(year) ? 29 : 28;
      int[] daysInMonths = new int[] { 31, daysInFeb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
      int stopIdx = month - 1; //1..12 -> 0..11
      int ax = 0;
      
      for (int i=0; i<stopIdx; ++i)
      {
        ax += daysInMonths[i];
      }      
      ax += day;

      return ax;
    }


    static long SubMonthsDaysToSec(int y1, int m1, int d1, int y2, int m2, int d2)
    {
      long dd1 = ToDayInYear(y1, m1, d1);
      long dd2 = ToDayInYear(y2, m2, d2);
      
      return (dd1 - dd2) * SecPerDay;
    }


    static long Sub(DateTime dt1, DateTime dt2)
    {
      long dYearsSec = SubYearsToSec(dt1.Year, dt2.Year);
      long dDaysSec = SubMonthsDaysToSec(dt1.Year, dt1.Month, dt1.Day, dt2.Year, dt2.Month, dt2.Day);
      long dTime = ((long)(dt1.Hour - dt2.Hour) * 60L + (long)(dt1.Minute - dt2.Minute)) * 60L + (long)(dt1.Second - dt2.Second);
      
      return dYearsSec + dDaysSec + dTime;
    }
    
    
    static long ToTime_T(DateTime dateTime)
    {
      DateTime unixEpochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
      
      return Sub(dateTime, unixEpochTime);
    }


    static void Main(string[] args)
    {
      DateTime now = new DateTime(2010, 12, 15, 14, 48, 30, DateTimeKind.Utc);
      long time_t = ToTime_T(now);
      Console.WriteLine("time_t = {0}", time_t);
      
      TimeSpan span = now - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
      long referenceTime = (long)span.TotalSeconds;
      Console.WriteLine("reference time_t = {0}", referenceTime);
    }
    
  } //class
} //namespace


* This source code was highlighted with Source Code Highlighter.


Замечу, что в программе есть места, которые можно несколько улучшить, в частности функцию ToDayInYear можно было бы написать и без использования цикла, но предпочту предоставить это читателю.

Перенаправление бинарного вывода процесса

В .Net для запуска одного процесса из под другого используется класс System.Diagnostics.Process.
Ниже приведен пример того, каким образом нужно перенаправлять БИНАРНЫЙ! вывод дочернего процесса.
В своем примере я запускаю утилиту "icat" из пакета "The Sleuth Kit", которая должна быть запущена от имени суперпользователя.

      ProcessStartInfo si = new ProcessStartInfo("icat", "<аргументы командной строки для утилиты icat>");
      si.UseShellExecute = false; //необходимо, чтобы получить возможность перенаправлять потоки
      si.RedirectStandardOutput = true; //сообщаем о том, что мы будем перенаправлять именно поток вывода
      si.UserName = "root"; //нужно, если собираетесь запускать вызываемую программу от имени другого пользователя
      si.Password = new System.Security.SecureString(); //ваш пароль, то же что и для UserName
      Process proc = new Process();
      proc.StartInfo = si;
      proc.Start();
      
      //proc.WaitForExit(); а вот этого делать нельзя!
      //WaitForExit можно вызывать только после полного считывания выходного потока
      
      using (var r = new BinaryReader(proc.StandardOutput.BaseStream))
      using (var w = new BinaryWriter(File.Create("<путь к фалу, куда направить output>")))
      {
        //копируем содержимое
      }

* This source code was highlighted with Source Code Highlighter.