воскресенье, 6 марта 2011 г.

MonoDevelop + NUnit (Fedora 14)

Согласно информации с официального сайта MonoDevelop (http://monodevelop.com/Download/What%27s_new_in_MonoDevelop_2.4), NUnit интегрирован в IDE.
Вот как это должно выглядеть:





Однако, когда мне понадобилось написать юнит-тесты для своего проекта, никакой интеграции я не обнаружил. Ни в списке "Add-in Manager'а" ни, даже, в "Edit References" диалоге нет и упоминания NUnit.
На моей Fedora 14 установлены:
  • mono-core v.2.6.7-3.fc14 + auto-detected dependencies (incl. mono-nunit, mono-nunit-devel)
  • monodevelop v.2.4-1.fc14 + auto-detected dependencies
Разбираясь с проблемой я обнаружил, что при загрузке
"/usr/lib/monodevelop/AddIns/NUnit/MonoDevelop.NUnit.dll"
MonoDevelop не может найти следующие referenced-библиотеки:
  • nunit.core.dll
  • nunit.core.interfaces
  • nunit.framework.dll
  • nunit.util.dll
несмотря на то, что они зарегистрированы в GAC (/usr/lib/mono/gac/).
Решать проблему использованием мягких или жестких ссылок не хотелось.
Поначалу, я думал, что сборки были как-то криво зарегистированы в GAC и пытался найти решение работая в этом направлении, но никакие попытки перерегистрировать эти библиотеки через gacutil успеха не принесли.

В итоге я сдался и все-таки создал мягкие ссылки рядом с MonoDevelop.NUnit.dll на библиотеки, которые MonoDevelop не мог найти.
ln -s [path to assembly in gac]/nunit.core.dll /usr/lib/monodevelop/AddIns/NUnit/nunit.core.dll

ln -s [path to assembly in gac]/nunit.core.interfaces.dll /usr/lib/monodevelop/AddIns/NUnit/nunit.core.interfaces.dll

ln -s [path to assembly in gac]/nunit.framework.dll /usr/lib/monodevelop/AddIns/NUnit/nunit.framework.dll

ln -s [path to assembly in gac]/nunit.util.dll /usr/lib/monodevelop/AddIns/NUnit/nunit.util.dll
Как я уже упомянул выше, путь к GAC: /usr/lib/mono/gac/
однако ваши пути к GAC и MonoDevelop могут отличаться, например: /usr/local/lib/mono/gac и т.п., в зависимости от того, что и куда вы поставили.

Это полностью решило проблему: появились соответствующие элементы UI и даже nunit сборки в "Edit References" диалоге:




Стоит правда упомянуть, что MonoDevelop все еще использует nunit-2.4.8 в то время как уже выпущена 2.5.9 и по-видимому переход на более новый nunit состоится нескоро и зависит от активности сообщества.

воскресенье, 9 января 2011 г.

Finalizer, Как работает GC?

В .Net GC реализует алгоритм "Пометить, Освободить, Упаковать" описанный например здесь. Изначально GC рассматривает все объекты как недостижимые. В процессе прохода по графу он помечает все достижимые объекты флажком, а все оставшиеся, соотвтетственно, подлежат сборке. Такой алгоритм осуществляется в один проход и позволяет легко определять и уничтожать "острова изоляции", а также не подвержен проблеме зацикленных ссылок.
Значительный интерес представляет то, как GC поступает с объектами, имеющими файналайзеры. Можно ли из файналайзера обращаться к другим reference type объектам? Могут ли эти reference type объекты оказаться уже уничтоженными и что тогда произойдет?
Давайте определим термины:
завершенными мы будем называть объекты для которых были вызваны их файналайзеры.
уничтоженными или собранными мы будем называть те объекты, память занимаемая которыми была освобождена и возвращена куче.
Сразу же скажу, что, в отличии от C++, обратиться к уничтоженным объектам в .Net н е в о з м о ж н о. Зато обращение к завершенным объектам вполне допустимо, более того, они даже могут быть "воскрешены".

На нижеприведенной диаграмме приблизительно описан жизненный цикл reference type объектов:



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

Условные обозначения на диаграмме:
Finalization Queue - очередь финализации (терминология из MSDN)
Freachable Queue - согласно терминологии MSDN, это список объектов готовых к завершению (финализации). Объекты попавшие в этот список, а так же все объекты на которые они ссылаются и так далее, не подлежат уничтожению и переживают сборку мусора. Поэтому из файналайзера можно смело обращаться к любым объектам, все они живы, другое дело что они могут оказаться уже завершенными.
Рассмотрим следующую ситуацию:

class ClassA
{
  bool _disposed;

  public void DoSomething()
  {
    if (_disposed)
    throw new ObjectDisposedException("ClassA");
  }

  ~ClassA()
  {
    _disposed = true;
  }
} //class


class ClassB
{
  ClassA _classA;

  public ClassB()
  {
    _classA = new ClassA();
  }

  ~ClassB()
  {
    _classA.DoSomething();
  }
} //class

* This source code was highlighted with Source Code Highlighter.

К моменту когда ClassB в файналайзере обращается к ClassA.DoSomething экземпляр ClassA может оказаться уже завершенным и в этом случае метод DoSomething кинет ObjectDisposedException. Если исключение корректно обработается в вызывающем коде, то никаких проблем нет. Если же оно "пролетит мимо" то это может привести к аварийному завершению всего приложения согласно тексту из MSDN. Вот почему в файналазерах необходимо осторожно работать с обращениями к другим reference type объектам.

Теперь об оживлении.
В файналайзере объект имеет право передать другому, в том числе и живому объекту ссылку на самого себя. В этом случае он и все объекты на которые он ссылается вновь окажутся доступными для кода программы и как бы воскреснут. Правда восрешенный объект не будет больше состоять в Finalization Queue и когда он следующий раз окажется недоступным, его файналайзер больше не будет вызван и объект окажется просто уничтоженным. Для того, чтоб вновь поставить объект в очередь финализации нужно воспользоваться вызовом
GC.ReRegisterForFinalize
Хотя лично мне подобный трюк не нравится потому, что он, например, может привести к появлению бессмертных Дунканов Маклаудов, утечкам памяти и излижним нагрузкам на GC.

Ниже приведен пример кода в котором объект восстанавливается в файналайзере:

class RecoverableObject
{
  int _id;
  static int Counter;
  bool _canDie;

  public static RecoverableObject Root;

  static RecoverableObject()
  {
    Root = new RecoverableObject();
    Root.CanDie = true;
  }

  public RecoverableObject()
  {
    _id = ++Counter;
  }

  ~RecoverableObject()
  {
    if (CanDie)
    {
      Console.WriteLine("good bye Id={0}", _id);
      return;
    }

    Console.WriteLine("obj.Id={0} is going to be recovered", _id);
    Root.Other = this;
    GC.ReRegisterForFinalize(this);
  }

  public int Id
  {
    get
    {
      return _id;
    }
  }

  public bool CanDie
  {
    get
    {
      return _canDie || this == Root;
    }
    set
    {
      _canDie = value;
    }
  }

  public RecoverableObject Other { get; set; }



  static void RecoverableObjectTest()
  {
    RecoverableObject obj;

    obj = new RecoverableObject();
    Console.WriteLine("first collection...");
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("after the first collection");
    Console.WriteLine();

    obj = RecoverableObject.Root.Other;
    RecoverableObject.Root.Other = null;
    Console.WriteLine("obj.Id={0} is available again", obj.Id);
    Console.WriteLine("second collection...");
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("after the second collection");
    Console.WriteLine();

    obj = RecoverableObject.Root.Other;
    RecoverableObject.Root.Other = null;
    Console.WriteLine("obj.Id={0} is available again", obj.Id);
    obj.CanDie = true;
    Console.WriteLine("third collection, obj.Id={0} must die!", obj.Id);
    GC.Collect();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("after the third collection");
    Console.WriteLine();

    Console.WriteLine("RecoverableObjectTest exiting...");
  }

} //class



* This source code was highlighted with Source Code Highlighter.

Вкратце, вот что нужно знать о финализации:
  1. Все объекты, имеющие метод Finalize помещаются в очередь финализации при их создании. Уничтожение объектов помещенных в очередь финализации производится как минимум в два этапа (может быть и больше, если их оживлять)
  2. Метод Finalize не должен выдавать исключения, поскольку они не могут обрабатываться приложением и могут привести к завершению работы приложения.
  3. Реализация методов Finalize (деструкторов) может отрицательно воздействовать на производительность, и злоупотреблять ими не рекомендуется.


Данный пост отвечает на вопрос, поставленный в посте "Qyoto, Issues for investigation" о том правомерно ли обращаться из файналайзера к reference type объектам. Отвечает утвердительно: "Да, правомерно"

В ближайших статьях я постараюсь дать ответы на оставшиеся вопросы.

четверг, 6 января 2011 г.

Qyoto, issues for investigation

Я выкачал исходники Qyoto и нашел в них несколько моментов, требующих более глубокого изученения:
  1. P/Invoke. Все managed обертки над Qt-шными классами содержат в себе член SmokeInvokation, через который осуществляются вызовы unmanaged кода и вызовы managed callback функций. Внутри этого класса все вызовы осуществляются через P/Invoke. Насколько это снизит производительность приложения? Предварительные тесты (на .Net Framework 4.0 показали), что вызов unmanaged функции без параметров через P/Invoke при отстутствии нагрузки на GC осуществляются в 50-70 раз медленнее, чем вызов аналогичной managed функции. Если в среднем managed код исполняется в 2-3 раза медленнее чем unmanaged и при этом он будет еще и осуществлять многократные вызовы через P/Invoke, то такой код будет работать в (2..3)*(50..70) = (100..200) раз медленнее чем unmanaged. Для нехитрого GUI со стандартными контролами это все равно кажется некритичным, но если вы, например, собираетесь отображать геоинформационные данные и ваши требования к производительности GUI высоки, то такое замедление видится неприемлемым. Более того, оно будет даже более существенным в случае маршаллинга данных и высоких нагрузок на GC. Тем не менее нужно изучить каковы накладные расходы на P/Invoke в Mono, может статься, что они не так уж велики.
  2. Следующий момент, это вызов деструкторов Qt-шных объектах в файналайзерах их managed оберток. Во-первых файналайзеры создают дополнительную нагрузку на GC и снижают производительность. Во-вторых в классах, поддерживающих интерфейс IDisposable, в методах Dispose() тоже вызываются деструкторы unmanaged объектов, но сами обертки при этом не помечаются как не нуждающиеся в финализации вызовом GC.SuppressFinalize(). В третьих, обращение к reference type объектам (SmokeInvokation) из файналайзеров кажется мне непрвильным, потому, что порядок уничножения reference type объектов недетерменирован и, таким образом, SmokeInvokation объект к моменту обращения уже может оказаться собранным GC. Это тоже нужно проверить.
  3. И наконец, если ref type объект был создан в некоторой функции и начиная с некоторого момента в создавшей его функции больше не будет обращений к нему, то GC разрешено собрать его, не дожидаясь выхода потока управления из данной, создавшей объект функции (я добавлю здесь пояснения). Таким образом может получится, что файналайзер объекта будет вызван из потока GC в то время пока unmanaged код все еще будет работать с его unmanged ресурасами. Чтоб предотвратить такую ситуацию нужно использовать HandleRef или GCHandle совместно с GC.KeepAlive. Нужно также проверить, как в Qyoto разруливается описанная ситуация и предусмотренна ли она вообще.

воскресенье, 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