Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

22/03/2013

Konwencja wołania

Home

Konwencja wołania (ang. Calling convention) to zestaw zasad, który określa w jaki sposób metoda wołająca przekazuje parametry do metody wołanej i odbiera od niej wyniki. Programując w .NET, o ile nie współpracujemy z kodem natywnym, w ogóle nie musimy się tym interesować. Zgłębiając ten temat można jednak nauczyć się kilku ciekawych rzeczy.

Zacznijmy od tego jakiej konwencji wołania używa CLR. Na to pytanie można odpowiedzieć na dwa sposoby. Odpowiedź "wysoko poziomowa" brzmi: CLR został zaimplemntowany jako maszyna stosowa i nie używa rejestrów, a więc parametry oraz wyniki zostaną przekazane na stosie. Oto prosty przykład, zacznijmy od klasy:

public class Test
{
        public string ReturnString(int i, bool b)
        {
            return i.ToString() + b.ToString();
        }

        public string ReturnString(Fun f)
        {
            return f.ToString();
        }
}

Teraz spójrzmy na kod w C# i odpowiadający mu kod IL, w którym użyto tych metod:

var t = new Test();

newobj instance void ConsoleApplication1.Test::.ctor()
stloc.0 

var res1 = t.ReturnString(10, true);

ldloc.0 
ldc.i4.s 10
ldc.i4.1 
callvirt instance string ConsoleApplication1.Test::ReturnString(int32, bool)
stloc.1 

var res2 = t.ReturnString(new Fun());

ldloc.0
newobj instance void ConsoleApplication1.Fun::.ctor()
callvirt instance string ConsoleApplication1.Test::ReturnString(class ConsoleApplication1.Fun)
stloc.2 

Istotne fragmenty zaznaczyłem na czerwono. ldloc.0 odpowiada za wrzucenie na stos obiektu, na rzecz którego zostanie wywołana metoda (this). Potem wrzucamy argumenty dla wołanej metody czyli ldc.i4.s 10 oraz ldc.i4.1. Po wywołaniu metody zdejmujemy natomiast wynik ze stosu i zapisujemy w zmiennej lokalnej o indeksie 1 przy pomocy komendy stloc.1. Dla drugiego wywołania wygląda to podobnie. Polecenie newobj tworzy nowy obiekt i umieszcza referencję do niego na stosie.

Czemu jednak CLR został zaimplementowany jako automat stosowy (maszyna wirtualna Java zresztą też), a rejestry pojawiają się dopiero kiedy program jest wykonywany i IL jest zamieniany na kod natywny? Otóż CLR to komercyjna implementacja VES (ang. Virtual Execution System). VES jest częścią standardu ECMA-335, który jest niezależny od docelowej architektury, a w szczególności nie wspomina nic o żadnych rejestrach. Dzięki temu konkretne implementacji standardu mogą używać rejestrów w dowolny sposób.

I tutaj dochodzimy do odpowiedzi "nisko poziomowej" na postawione pytanie, czyli jakiej konwencji używa kod natywny wygenerowany na podstawie IL dla Windows'a. Otóż jest to konwencja fastcall, w której na przykład dla architektury x86 dwa pierwsze parametry przekazywane są w rejestrach ECX oraz EDX. Więcej szczegółów na ten temat można znaleźć na blogu Joe Duffy'ego.

Od siebie dodam, że jeśli korzystamy z P/Invoke to domyślnie metody natywne wołane są przy pomocy konwencji Winapi, która w systemie Windows jest tożsama z Stdcall. Zachowanie to możemy zmienić przy pomocy właściwości CallingConvention atrybutu DllImportAttribute.

16/01/2013

Mono C# compiler as a service 3

Home

Ponownie wrócę do tematyki kompilowania C# w locie. Pisałem wcześniej, że potrzebowałem takiej funkcji aby użytkownicy mojej aplikacji mogli w dowolnym momencie zdefiniować własny algorytm obliczania odległości między dwoma wektorami.

Po kilku próbach już wiedziałem jak to zrobić, a chwilę później miałem już zaimplementowaną pierwszą wersję rozwiązania. Przyszła pora wypróbowania nowej zabawki na prawdziwych danych. Uruchomiłem więc aplikację, napisałem krótki skrypt i wystartowałem obliczenia. W tym momencie aplikacja zamarła i było widać, że pożera wszelkie dostępne zasoby. Spodziewałem się czegoś innego :). Skończyło się zabiciem aplikacji. Co zrobiłem nie tak?

Dane jakich użyłem zawierały kilkaset tysięcy wektorów. Oznacza to, że odległość między wektorami musiała zostać obliczona "dużoooooooooooooooooooooo" razy. Każda taka operacja powodowała wywołanie omówionej już metody Evaluator.Run, czyli za każdym razem ten skrypt był ponownie kompilowany, a to trochę trwa. Ziarnko do ziarnka, a zbierze się miarka jak mówi przysłowie. Co więcej każde wywołanie Evaluator.Run powoduje załadowanie do pamięci kolejnego dynamicznie utworzonego modułu. Moduły te są potem usuwane ale to też trwa. W ramach eksperymentu proponuję uruchomić pod kontrolą debugger'a taki kod:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

for (int i = 0; i < 1000; ++i)
 ev.Run("System.Console.WriteLine(\"Hello!\");");

Zamiast zobaczyć, że kod wykonuje się błyskawicznie, zaobserwujemy jak VS próbuje załadować symbole dla kolejnych modułów (komunikat w rodzaju Loading symbols for eval-120... na pasku stanu). Jeśli postawimy pułapkę wewnątrz pętli i otworzymy okno Modules to na liście znajdziemy np.: eval-100, eval-101, eval-102...

W takiej sytuacji skrypt należy skompilować raz, a potem wielokrotnie używać wyniku kompilacji. Służy do tego metoday Evaluator.Compile, która zwraca instancję klasy CompiledMethod. Jeśli chcemy wywołać tak przygotowany skrypt korzystamy z metody CompiledMethod.Invoke. Nic trudnego. Parametry do takiego skompilowanego skryptu można przekazać w dokładnie taki sam sposób jak opisałem w poprzednim poście. Po zmianach powyższy kod będzie wyglądał jak poniżej i wykona się błyskawicznie:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

var method = ev.Compile("System.Console.WriteLine(\"Hello!\");");

object o = null;
for (int i = 0; i < 1000; ++i)
 method.Invoke(ref o);

Na koniec dodam, że po zastosowaniu takiego zabiegu nie widzę różnicy w czasie odpowiedzi pomiędzy przypadkiem kiedy używam algorytmu osadzonego/wkompilowanego w aplikację, a algorytmu dostarczonego przez użytkownika.

13/01/2013

Mono C# compiler as a service 2

Home

Dzisiaj wrócę do tematu użycia języka C# jako języka skryptowego przy pomocy Mono.CSharp.dll i opiszę w jaki sposób przekazać parametry do takiego skryptu. Pominę podejście opierające się o wklejanie do skryptu string'owej reprezentacji takich parametrów i od razu przejdę do bardziej eleganckiego rozwiązania. Bazuje ono na tym co przeczytałem w tym poście.

Zaczynamy od utworzenia statycznej klasy ScriptContext, która posłuży nam do wymiany danych pomiędzy skryptem, a naszym programem.

namespace Scripting
{
    public static class ScriptContext
    {
        static ScriptContext()
        {
            Items = new Dictionary<string, object>();
        }

        public static object Result { get; set; }

        public static IDictionary<string, object> Items { get; set; }
    }
}

Użycie jej nie jest trudne. Mały przykład:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));
            
//Informujemy silnik skryptowy gdzie została zdefiniowana klasa ScriptContext
ev.ReferenceAssembly(typeof(ScriptContext).Assembly);

//Ustawiamy parametry
ScriptContext.Items["param1"] = new List<string> { "Hello!", "Welcome!", "Hi!" };

ev.Run("using System;");
ev.Run("using Scripting;");
//Korzystamy z przekazanych parametrów
ev.Run("foreach(var s in (System.Collections.Generic.IEnumerable<string>)ScriptContext.Items[\"param1\"]) Console.WriteLine(s);");

Właściwość ScriptContext.Result dodałem aby nie musieć zastanawiać się kiedy wywołać metodę Evaluator.Run, a kiedy Evaluator.Evaluate. Zawsze używam tej pierwszej, zakładając, że wynik ze skryptu zwracany jest przy użyciu tej właściwości np.:

ev.Run("ScriptContext.Result = 1;");
Console.WriteLine(ScriptContext.Result);

Do szczęścia brakowało mi jednak jeszcze jednej rzeczy. Jak pokazuje pierwszy przykład musiałem użyć rzutowania do IEnumerable<string> aby cieszyć się silnie typowanym parametrem. Napisałem więc metody pomocnicze, które przygotowują silnie typowane parametry:

public static class ScriptingHelper
{
 public static StringBuilder PrepareParameters(IDictionary<string, object> parameters)
 {
  ScriptContext.Items = parameters;

  var sb = new StringBuilder();

  if (parameters != null)
  {
   foreach (var kvp in parameters)
   {
    var typeName = ExtractTypeName(kvp);

    sb.AppendFormat("var {0} = ({1}){2}.Items[\"{0}\"];", kvp.Key, typeName, typeof(ScriptContext).Name);
    sb.AppendLine();
   }
  }

  return sb;
 }

 private static string ExtractTypeName(KeyValuePair<string, object> kvp)
 {
  if (kvp.Value == null)
   throw new Exception("null parameters are not supported!");
   
  var type = kvp.Value.GetType();

  using (var provider = new CSharpCodeProvider())
  {
   var typeRef = new CodeTypeReference(type);
   return  provider.GetTypeOutput(typeRef);
  }
 }
}

Nic bardzo skomplikowanego ale warto zwrócić uwagę na użycie klasy CSharpCodeProvider. Dzięki niej uzyskuję poprawną, pełną nazwę typów, również tych generycznych. Gdybym na przykład dla listy int'ów spróbował użyć czegoś w rodzaju list.GetType().Name to w wyniku otrzymałbym List`1, co do rzutowania się nie nadaje. Wcześniejszy przykład można więc napisać teraz w taki sposób:

//Ustawiamy parametry
var parameters = ScriptingHelper.PrepareParameters(new Dictionary<string, object>
{
 {"param1", new <string> { "Hello!", "Welcome!", "Hi!" } }
});

ev.Run("using System;");
ev.Run("using Scripting;");
ev.Run(parameters.ToString());
//Korzystamy z przekazanych parametrów ale już bez rzutowania
ev.Run("foreach(var s in param1) Console.WriteLine(s);");

Wracając do mojej aplikacji, w której chciałem umożliwić użytkownikowi definiowanie własnych algorytmów obliczania odległości między wektorami. Dane wejściowe to oczywiście dwa wektory. Dzięki takiemu podejściu, zamiast zmuszać użytkownika to rzutowania parametrów na określony typ, używam konwencji czyli: wektor numer 1 znajduje się w zmiennej vector1 itd.

Klasa ScriptingHelper jest również doskonałym miejsce na dodawanie różnych innych "przydasiów" ułatwiających pracę użytkownikowi.

12/01/2013

Mono C# compiler as a service

Home

Od jakiegoś czasu pracuję nad aplikacją do generowania i analizowania wykresów rekurencyjnych. Temat sam w sobie jest bardzo ciekawy, więc może do niego wrócę w przyszłości, ale dzisiejszy post będzie o czymś innym.

Moja aplikacja między innymi wykonuje obliczenia na wektorach np.: oblicza różne odległości (euklidesową, Manhattan czy normę maksimum) między nimi. Dodawanie kolejnych algorytmów wymagało jednak każdorazowej rekompilacji aplikacji. Zacząłem więc szukać sposobu aby umożliwić użytkownikowi dodawanie własnych algorytmów w sposób dynamiczny. Innymi słowy potrzebowałem jakiegoś silnika skryptowego.

Początkowo pomyślałem o PowerShell'u, rozważałem również IronPython'a. Trafiłem jednak na krótki i treściwy post na temat biblioteki Mono.CSharp.dll, która stanowi część dobrze znanego projektu Mono. Opis wyglądał obiecująco dlatego postanowiłem wypróbować tą bibliotekę i był to strzał w dziesiątkę.

Jądrem biblioteki jest klasa Mono.CSharp.Evaluator, która służy do dynamicznego kompilowania i wykonywania kodu C#. Jej użycie jest wręcz trywialne. Oto prosty przykład Hello World!:

var settings = new CompilerSettings();
var ev = new Evaluator(new CompilerContext(settings, new ConsoleReportPrinter()));

ev.Run("using System;");
ev.Run("Console.WriteLine(\"Hello World!\");");

Użycie ConsoleReportPrinter powoduje, że wszelkie komunikaty kompilatora zostaną wypisane na ekran konsoli. Możemy też użyć klasy StreamReportPrinter, wtedy komunikaty zostaną zapisane do wskazanego strumienia. Klasa CompilerSettings pozwala natomiast skonfigurować różne aspekty pracy kompilatora. Kolejny prosty przykład pokazuje, jak zwrócić wynik ze skryptu:

ev.Run("Console.Write(\">\")");
ev.Run("var s = Console.ReadLine();");
var s = (string)ev.Evaluate("s;");

Ciekawe jest to, że możemy zmienić typ raz zadeklarowanej zmiennej np.:

ev.Run("int s = 1;");
ev.Run("Console.WriteLine(s);");
ev.Run("string s = \"Hello!\";");
ev.Run("Console.WriteLine(s);");
ev.Run("bool s = true;");
ev.Run("Console.WriteLine(s);");

Możliwe jest również zdefiniowanie klasy:

ev.Run("class Test { public string Fun(int i) { return \"Hello \" + i; } }");
ev.Run("Console.WriteLine(new Test().Fun(1));");

Załóżmy, że w oddzielnym projekcie mamy zdefiniowaną klasę Test2. Poniższy przykład pokazuje jak użyć jej w skrypcie:

ev.ReferenceAssembly(typeof(Test2).Assembly);
ev.Run("using TestLib;");
ev.Run("Console.WriteLine(new Test2().Welcome(\"Kate\"));");

Praca z klasą Mono.CSharp.Evaluator bardzo mi się podoba. To kawał dobrej roboty, zachęcam więc do wypróbowania. W kolejnych postach opiszę, jak w elegancki sposób przekazać argumenty do skryptu oraz kiedy warto skorzystać z metody Compile zamiast Run lub Evaluate.

Uwaga instalacyjna

Pisząc ten post korzystałem z wersji beta 3.0.3 Mono. Ostatnia dostępna stabilna wersja 2.10.9 zawiera bowiem błąd, który objawia się komunikatem:

Method 'Mono.CSharp.Location.ToString()' is security transparent, but is a member of a security critical type.

przy pierwszej próbie użycia klasy Evaluator.  Nie sprawdzałem ale możliwe, że z github'a można ściągnąć wersję bez tego błędu.

29/10/2012

Trochę więcej o serializacji

Home

W poprzednim poście pisałem o serializacji XML'owej, a w tym wrócę jeszcze do tematu. Skorzystamy z tego samego kodu co poprzednio ale tym razem uruchomimy go korzystając z innych serializatorów. Zamieńmy, więc XmlSerializer na SoapFormatter lub BinaryFormatter i uruchommy przykładowy kod jeszcze raz (można użyć też DataContractSerializer ale z drobną modyfikacją kodu).

Efekt końcowy będzie inny niż dla serializatora XML'owego - po deserializacji właściwość Name będzie miała taką samą wartość jak przed serializacją. To nie jedyna różnica. Jeśli do konstruktora domyślnego dodamy linijkę:

Console.WriteLine("I'm in a constructor");

i popatrzymy na liczbę wypisanych na ekran napisów I'm in a constructor to okaże się, że w przypadku serializatora XML'owego konstruktor domyślny został wywołany dwa razy (przy tworzeniu obiektu oraz przy deserializacji), a w przypadku pozostałych klas tylko jeden raz (przy tworzeniu obiektu).

Należy postawić pytanie, jak w takim razie pozostałe serializatory tworzą obiekty przy deserializacji. Przecież muszą wołać jakiś konstruktor. Otóż nie koniecznie. O ile wiem taki BinaryFormatter, pozostałe serializatory korzystają zapewne z podobnych mechanizmów, używa metody FormatterServices.GetSafeUninitializedObject, która jest odpowiedzialna za zaalokowanie odpowiednio dużego fragmentu pamięci dla obiektu zadanego typu.

Po co to wszystko? Czy nie łatwiej wywołać konstruktor? Początkowo pomyślałem, że to kwestia wydajności. Ale szybki test pokazuje, że użycie FormatterServices.GetSafeUninitializedObject jest troszkę wolniejsze niż utworzenie obiektu za pomocą konstruktora.

var s = new Stopwatch();
s.Start();

for(int i = 0; i < 1000000; ++i)
 FormatterServices.GetSafeUninitializedObject(typeof(A));

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~130 ms

s.Reset();
s.Start();

for (int i = 0; i < 1000000; ++i)
    new A();

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~20 ms
Po chwili zastanowienia doszedłem do innego wniosku. Otóż, jeśli na proces deserializacji popatrzymy jak na proces "przywracania do życia" już raz utworzonych obiektów, to ponowne wywołanie konstruktora po prostu nie ma sensu albo wręcz jest niewskazane. Macie jakieś inne pomysły? Jak sądzicie czemu jedne serializatory wołają konstruktor, a inne nie?

27/10/2012

XmlSerializer- taka ciekawostka

Home

Serializator XML'owy platformy .NET jest bardzo łatwy i przyjemny w użyciu, ale czasami jego działanie może sprowadzić nas na manowce. Poniższy kod obrazuje o co mi chodzi. Zacznijmy od przykładowej, bardzo prostej klasy, którą będziemy serializować:

public class A
{
 public string Name { get; set; }
 
 public A()
 {
  Name = "Hello"; 
 }
}

Oraz kawałka kodu:

var obj = new A() { Name = null };

var stream = new MemoryStream();
var serializer = new XmlSerializer(typeof(A));

serializer.Serialize(stream, obj);

stream.Position = 0;

var obj2 = (A)serializer.Deserialize(stream);

Kod jest bardzo prosty. Tworzymy obiekt klasy A i ustawiamy właściwość Name na null. Następnie zapisujemy obiekt do strumienia. Po zapisaniu przesuwamy wskaźnik odczytu/zapisu strumienia na początek aby móc zdeserializować obiekt. W czym tkwi haczyk? Spróbujcie odpowiedzieć na to pytanie bez zaglądania do dalszej części posta:

Jaka będzie wartość właściwości Name po odczytaniu obiektu ze strumienia?

Otóż po zdeserializowaniu właściwość Name nie będzie pusta. Serializator XML'owy odczytując obiekt ze strumienia najpierw wywoła konstruktor domyślny. Potem zobaczy, że w strumieniu właściwość Name jest pusta i stwierdzi, że w takim wypadku nie trzeba niczego przypisywać do właściwości Name deserializowanego obiektu. W konstruktorze znajduje się natomiast kod, który ustawia tą właściwości. A więc po zdeserializowaniu właściwość Name będzie zawierała ciąg znaków "Hello", czyli inną niż przed serializacją.

Teraz wyobraźmy sobie trochę bardzie skomplikowaną sytuację. Załóżmy, że mamy kod pobierający jakieś dane z bazy danych i ładujący je do odpowiednich klas. Klasy te są napisane w podobny sposób jak klasa A powyżej, czyli w konstruktorze domyślnym niektóre właściwości są inicjowane wartościami innymi niż null. Dane (obiekty) są następnie udostępniane aplikacji klienckiej przez web service'y (stare dobre ASMX, usługi WCF jeśli użyto XmlSerializerFormatAttribute)  czyli muszą być najpierw zserializowane, a potem zdeserializowane. Kod działa poprawnie.

Teraz zabieramy się za pisanie testów integracyjnych i używamy tego samego kodu odpowiedzialnego za wczytywanie danych. W momencie, kiedy chcemy użyć tych samych obiektów (tak samo stworzonych i wypełnionych danymi) co aplikacja kliencka, nawet w ten sam sposób, okazuje się, że dostajemy wyjątek NullReferenceException.

Dlaczego? Ano dlatego, że w testach integracyjnych obiekty nie zostały przesłane przez sieć, czyli nie były serializowane i deserializowane, czyli konstruktor domyślny nie został wywołany drugi raz przy deserializacji i puste właściwości nie zostały ustawione tak jak opisano powyżej.

Jak w wielu innych podobnych sytuacjach, to żadna wiedza tajemna, ale jak się o tym nie wie, może napsuć trochę krwi. Pomijam tutaj fakt, że korzystanie z opisanej "funkcjonalności", świadomie czy nie, nie jest najlepszym pomysłem.

14/09/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy 2

Home

Niedawno kolega opowiedział mi o jeszcze jednym przypadku kiedy opisane przeze mnie zachowanie konstruktora statycznego w środowiskach x86/x64 doprowadziło do kłopotów. Scenariusz był dość ciekawy, dlatego go opiszę na uproszczonym przykładzie. Zacznijmy od tego, że napisaliśmy zarządzany komponent COM. Komponent ten w konstruktorze statycznym czyta wartość jakiegoś parametru konfiguracyjnego z pliku i na tej podstawie coś robi. W poniższym przykładzie, żeby nie komplikować sprawy, po prostu zapisuje jego wartości do pliku:

[ComVisible(true)]
public class MySimpleCOM : ServicedComponent
{
 static MySimpleCOM()
 {
  File.AppendAllText(
   @"c:\log.txt",
   String.Format("Value of configuration parameter = '{0}' in the domain '{1}'\n.", ConfigurationManager.AppSettings["Test"], AppDomain.CurrentDomain.FriendlyName));
 }
 
 public MySimpleCOM() {}
 public void Fun() {}
}

Dodam jeszcze, że do pliku AssemblyInfo.cs w projekcie z naszym zarządzanym komponentem zostały dodane takie atrybuty:

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]

ApplicationActivation zapewnia, że nasz komponent będzie hostowany poza procesem, w którym się do niego odwołamy, dokładniej przez proces dllhost.exe. Drugi atrybut jest opcjonalny ale ułatwia testy i mówi, że nie jest wymagana żadna kontrola dostępu (uprawnień) kiedy ktoś będzie próbował użyć naszego komponentu. Kod testujący nasz komponent jest jeszcze prostszy:

class Program
{
 static void Main(string[] args)
 {
  using (var host = new MySimpleCOM())
  {
   host.Fun();
  }
 }
}

Warto zwrócić uwagę, że nie trzeba samemu rejestrować komponentu. Zostanie to zrobione za nas.

Na koniec dodajemy jeszcze do pliku konfiguracyjnych programu testującego (app.config) oraz komponentu COM (Application.config) sekcję appSettings z parametrem test. Dla rozróżnienia niech wartość tego parametru różni się w obu przypadkach np.:

<appSettings>
 <add key="test" value="Tester"/>
</appSettings>

oraz

<appSettings>
 <add key="test" value="COM"/>
</appSettings>

Aby zobaczyć na czym dokładnie polega problem najłatwiej postąpić w następujący sposób. Najpierw kompilujemy oba projekty z opcją x86, uruchamiamy i zaglądamy do pliku z log.txt. Dalej kompilujemy ponownie oba projekty z opcją x64 i znowu uruchamiamy pamiętając aby uprzednio odinstalować wersję x86 komponent COM. Można to zrobić na przykład przy pomocy narzędzia Usługi składowe (dcomcnfg) wbudowanego w system Windows.

W pierwrzym przypadku (x86) plik log.txt będzie wyglądał tak:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.

A w drugim (x64) w następujący sposób:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.
Value of configuration parameter = 'Tester' in the domain 'Tester.vshost.exe'.

Jak widać w przypadku środowiska x64 kod inicjalizujący w konstruktorze statycznym został wywołany dwa razy, do tego z różnymi wartościami parametru konfiguracyjnego. W prawdziwym systemie mogłoby to doprowadzić do powstania jakichś błędów i chyba sami przyznacie, że ich przyczyna nie byłaby trywialna do wykrycia.

PS.

Jeśli przy własnoręcznym odtwarzaniu tego scenariusza napotkacie problem, że komponent COM nie będzie czytał konfiguracji ze pliku Application.config, to odsyłam na przykład do tego wpisu, w którym wszystko jest wyjaśnione.

08/07/2012

WPF i opóźnione wykonanie (deferred execution)

Home

Najpierw spójrzmy na poniższy fragment XAML'a z prostą implementacją combobox'a z wieloma kolumnami:

...
<ComboBox ItemsSource="{Binding PerformedAnalysis}" SelectedValuePath="Id" SelectedValue="{Binding SelectedItem}">
 <ComboBox.ItemTemplate>
  <DataTemplate>
   <StackPanel Orientation="Horizontal">
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Id}" Width="100"></TextBlock>
    </Border>
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Name}" Width="500"></TextBlock>
    </Border>
   </StackPanel>
  </DataTemplate>
 </ComboBox.ItemTemplate>
</ComboBox>
...

Oraz kod z view model'u zasilający tą kontrolkę. Kod ten używa Entity Framework, a Context to nic innego jak obiekt dziedziczący po ObjectContext.

...
private IEnumerable _PerformedAnalysis = null;

public IEnumerable PerformedAnalysis
{
    get
    {
        try
        {
            if (_PerformedAnalysis == null)
            {
                _PerformedAnalysis = 
                        from a in Context.Analyses
                        orderby a.Id
                        select new { Id = a.Id, Name = a.Name };
            }
        }
        catch (Exception ex)
        {
            ServiceProvider.GetService<IWindowService>().ShowError(ex);
            _PerformedAnalysis = new[] { new { Id = -1, Name = ex.Message } };
        }

        return _PerformedAnalysis;
    }
}
...

Kod ten zawiera dość poważną usterką. Jaką? Ktoś może powiedzieć, że błędem jest użycie bezpośrednio EF'a, zamiast ukryć dostęp do danych za warstwą interfejsów. W prostej aplikacji nie ma to moim zdaniem sensu, w bardziej rozbudowanej też można nad tym dyskutować. Ktoś może również powiedzieć, że przy bindowaniu należy użyć ObservableCollection. W tym jednak przypadku do kolekcji nie będą dodawane lub usuwane żadne elementy, więc nie jest to konieczne.

Błąd jest dużo poważniejszy i może spowodować, potocznie mówiąc, wywalenie się całej aplikacji. Odpowiedzmy na pytanie co się stanie jeśli nawiązanie połączenia z bazą danych jest niemożliwie? Oczywiście zostanie zgłoszony błąd ale czy zostanie obsłużony? Pozornie powyższy kod poradzi sobie z takim scenariusz, przecież zawiera blok try/catch.

Zapomniano jednak o tzw. opóźnionym wykonaniu (ang. deffered execution) czyli o tym, że właściwe zapytanie do bazy danych zostanie wykonane dopiero kiedy dane będą rzeczywiście potrzebne czyli kiedy silnik WPF wykona bindowanie. Inaczej mówiąc wyjątek spowodowany brakiem połączenia z bazą danych zostanie rzucony gdzieś wewnątrz silnika WPF. Będzie można go przechwycić korzystając z DispatcherUnhandledException ale z perspektywy działania aplikacji to już nic nie da.

Jak to często bywa naprawienie błędu jest bardzo proste. Należy zapewnić wykonanie zapytania jeszcze w naszym kodzie np.:

...
 _PerformedAnalysis = 
  (from a in Context.Analyses
  orderby a.Id
  select new { Id = a.Id, FileName = a.FileName, Start = a.StartTime }).ToList();
...

09/05/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy

Home

Na początku marca pisałem o "dziwnym" zachowaniu konstruktora statycznego. W skrócie chodziło o to, że jeśli chcieliśmy wywołać metodę w domenie aplikacyjnej innej niż domyślna, konstruktor statyczny klasy, do której należała ta metoda, wołany był o jeden raz więcej w środowisku x64 niż w środowisku x86. Ponieważ sprawa mnie nurtowała postanowiłem zadać pytanie poprzez portal Microsoft Connect. Zajęło to trochę ale w końcu uzyskałem wyjaśnienie. Odpowiedź jest dość ciekawa, a więc zapraszam do lektury.

03/03/2012

Domeny aplikacyjne, konstruktor statyczny, a platforma x86 vs x64

Home

Na początek trochę kodu. Zacznijmy od klasy testowej:
public class TestClass : MarshalByRefObject
{
    static TestClass()
    {
        Console.WriteLine(String.Format("I'm in the static constructor in the domain '{0}'.",
            AppDomain.CurrentDomain.FriendlyName));
    }

    public void Hello()
    {
        Console.WriteLine(String.Format("Hello from the domain '{0}'.", AppDomain.CurrentDomain.FriendlyName));
    }
}
Teraz kod testujący:
AppDomain domain = AppDomain.CreateDomain("Test");

TestClass t = (TestClass)domain.CreateInstanceAndUnwrap(typeof(TestClass).Assembly.FullName, typeof(TestClass).FullName);
t.Hello();
Oraz pytanie co zostanie wypisane na ekran? A w szczególności ile razy zostanie wywołany konstruktor statyczny? W głównej domenie aplikacyjnej? W domenie pomocniczej? A może w obu?

Skoro o to pytam to zapewne gdzieś tkwi haczyk. Otóż okazuje się, że wynik będzie zależał od tego czy program został skompilowany z opcję Platform target ustawioną na x86 czy x64. W przypadku x86 na ekran zostanie wypisany taki wynik:

I'm in the static constructor in the domain 'Test'.
Hello from the domain 'Test'
A w przypadku x64 taki:
I'm in the static constructor in the domain 'Test'.
I'm in the static constructor in the domain 'ConsoleApplication.vshost.exe'.
Hello from the domain 'Test'
Jeśli natomiast program zostanie skompilowany z opcją AnyCPU wynik będzie zależał od maszyny na jakiej go uruchomimy.

Konstruktor statyczny dla danej klasy wołany jest co najwyżej jeden raz w danej domenie aplikacyjnej. Jak jednak widać w zależności od platformy może zostać wywołany w jednej lub dwóch domenach. Może to mieć znaczenie kiedy jego kod będzie zawierał np.: jakiś kod inicjalizujący. Przedstawiony scenariusz nie jest zbyt częsty ale jeśli wystąpi, wykrycie błędu może być trudne, dlatego dobrze wiedzieć o tej różnicy.

Opisane zachowanie testowałem na trzech maszynach więc zakładam, że nie jest to coś lokalnego i przypadkowego.

12/02/2012

Problem z AssemblyResolve

Home

Niedawno miałem okazję pracować z aplikacją, w której zaimplementowano obsługę zdarzenia AppDomain.AssemblyResolve. Zdarzenie to pozwala załadować assembly jeśli standardowy mechanizm platformy .NET nie poradzi sobie z tym zadaniem. Aplikacja działała poprawnie aż do migracji na platformę .NET 4.0 Czemu? O tym właśnie będzie post. Zwrócę w nim uwagę na dość istotną różnicę pomiędzy platformą .NET 4.0, a jej wcześniejszymi wersjami jeśli chodzi o wspomniane zdarzenie. Różnica ta, w określonych warunkach, może napsuć krwi.

Przechodząc do meritum chodzi o to, że począwszy od .NET 4.0 zdarzenie AppDomain.AssemblyResolve generowane jest dla wszystkich assemblies, również tych z zasobami. Poniżej cytat z dokumentacji z MSDN'a.

Beginning with the .NET Framework 4, the ResolveEventHandler event is raised for all assemblies, including resource assemblies. In earlier versions, the event was not raised for resource assemblies. If the operating system is localized, the handler might be called multiple times: once for each culture in the fallback chain.

Oznacza to, że jeśli biblioteka SomeLibrary zawiera zasoby, to zdarzenie AppDomain.AssemblyResolve zostanie wygenerowane dla nie dwa razy. Raz z żądaniem załadowania assembly o nazwie SomeLibrary, a drugi raz z żądaniem załadowania SomeLibrary.resources. Fizycznie oba assembly znajdują się natomiast w dll'ce o nazwie SomeLibrary.dll.

Jeśli obsługa zdarzenia AppDomain.AssemblyResolve zaimplementowana została w uproszczony albo raczej błędy sposób mamy problem. Mam tu na myśli implementację, w której nie przewidziano, że nie uda się znaleźć dll'ki o nazwie zgodnej z nazwą assembly. W omawianym przypadku wyglądało to mniej więcej tak:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 Assembly asm = Assembly.LoadFrom(path);
 return asm;
}
Łatwo się domyślić, że wywołanie Assembly.LoadFrom(path) dla ścieżki SOME_PATH\SomeLibrary.resources.dll zakończy się błędem bo taka ścieżka nie istnieje. W aplikacji objawi się to wyjątkiem FileNotFoundException przy próbie odczytania czegokolwiek z zasobów. Powinno to zostać zrobione wcześniej ale łatwo to naprawić:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 try
 {
  return Assembly.LoadFrom(path);
 }
 catch(Exception ex)
 {
  //Log exception
  
  return null;
 }
}
Albo w taki sposób, jeśli chcemy obsłużyć tylko scenariusz z zasobami:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 if(args.Name.Contains(".resources,"))
  return null;
  
 Assembly asm = Assembly.LoadFrom(path);
 return asm;
}
Zwrócenie null powoduje, że zostanie użyty standardowy mechanizm wyszukiwania i ładowania assemblies platformy .NET, a ponieważ dll'ka SomeLibrary.dll zostanie załadowana przy pierwszym żądaniu to poradzi sobie on z załadowaniem assembly z zasobami.

11/03/2011

Kilka słów o operatorach

Home

Dzisiaj kolega podesłał mi swoistą zagadkę w postaci dwóch, pozornie robiących to samo, fragmentów kodu. Jeden napisany w C#, a drugi w VB.NET:
int counter = 0;
while (++counter < 10)
{
  Console.WriteLine(counter.ToString());
}
Dim counter As Integer = 0
While (++counter < 10)
  Console.WriteLine(counter.ToString())
End While
Różnica polega na tym, że pętla w C# wykona się 9 razy, a w VB.NET otrzymamy pętlę nieskończoną. Dlaczego? Przyjrzyjmy się dokładniej pętli napisanej w języku VB.NET. Ci z was, którzy programują lub programowali w tym języku zapewne zwrócili uwagę na użycia operatora preinkrementacji. No właśnie, od kiedy w VB.NET mamy operator preinkrementacji? I tu tkwi pies pogrzebany, w VB.NET nie ma operatora preinkrementacji czy też predekrementacji. W takim razie czemu ten kod w ogóle się kompiluje? Początkowo pomyślałem, że to błąd kompilatora. W przekonaniu tym utwierdził mnie dodatkowo reflector, który po wrzuceniu do niego dll'ki pokazał coś takiego:
...
Do While (counter < 10)
...
Efekt zaobserwowałem w VS 2010, a więc eksperyment przeprowadziłem jeszcze w VS 2008 z tym samym wynikiem. W tym momencie pomyślałem, że to jednak niemożliwe aby przez tyle lat taki bug pozostał niewykryty. Po chwili mnie olśniło i wszystko okazało się proste i oczywiste. Zanim rozwiążę zagadkę do końca powiem, że poniższy kod również się skompiluje:
Dim counter As Integer = 0

While (++-+---++--+-+-counter < 10)
  Console.WriteLine(counter.ToString())
End While
C# nie jest gorszy i w nim również da się stworzyć podobnego dziwoląga:
int counter = 0;
while (+-+-+-+-+--counter < 10)
{
  Console.WriteLine(counter.ToString());
}
W tej chwili wszystko powinno być już jasne. + i - to nie tylko operatory binarne ale również operatory unarne. Poniżej fragment dokumentacji z MSDN:

Unary + operators are predefined for all numeric types. The result of a unary + operation on a numeric type is just the value of the operand.

Unary - operators are predefined for all numeric types. The result of a unary - operation on a numeric type is the numeric negation of the operand.

W języku VB.NET, tak jak pisałem wcześniej, nie ma operatorów preinkrementacji oraz predekrementacji, a więc zmienną liczbową możemy poprzedzić dowolną kombinacją + i -. W języku C# takie operatory istnieją, a więc zmienna liczbowa może zostać poprzedzona ciągiem + i - pod warunkiem, że występują one naprzemiennie (z wyjątkiem ostatniej pary).


26/02/2011

Jak zagłodzić Timer?

Home

Okazuje się, że bardzo prosto, ale zacznijmy od początku. Niedawno zakończyłem pracę nad serwerem zajmującym się wykonywaniem tzw. zadań wsadowych. Definicje zadań do wykonania pobierane są z bazy danych, a w danym momencie może działać wiele serwerów. Każdy serwer rezerwuje sobie swoje zadania na pewien kwant czasu. Po upływie tego czasu inne serwery mają prawo przejąć to zadanie. Może się tak zdarzyć na przykład jeśli jakiś serwer ulegnie awarii. Jeśli wykonanie danego zadania zajmuje więcej czasu niż czas rezerwacji to serwer musi przedłużyć dzierżawę.

W tym celu stworzyłem komponent, który monitoruje zadania przetwarzane przez serwer i kiedy zbliża się czas wygaśnięcia dzierżawy, przedłuża ją. Komponent ten korzysta z klasy Timer z przestrzeni nazw System.Timers, która co określony kwant czasu generuje zdarzenie. W metodzie obsługującej to zdarzenia nie robię nic innego jak po prostu aktualizuję czas wygaśnięcia dzierżawy. Tyle w telegraficznym skrócie.

W czasie testowania stworzonego rozwiązania zauważyłem, że w niektórych przypadkach czas wygaśnięcia dzierżawy nie jest aktualizowany. Wyglądało na to jakby klasa Timer nie generowała zdarzeń albo generowała je z dużym opóźnieniem! Wbrew pozorom rozwiązanie zagadki okazało się bardzo proste. Otóż klasa Timer generuje zdarzenie Elapse w wątku pochodzącym z puli wątków ThreadPool. Jeśli dodamy do tego fakt, że zadania wsadowe również są wykonywane przez wątki z puli to wszystko staje się oczywiste. Jeśli serwer umieści w puli odpowiednio dużo zadań wsadowych to może okazać sie, że brakuje wątków dla klasy Timer.

Poniżej prosty kod prezentujący ten efekt. Na początku ograniczam liczbę wątków do 10 i zlecam wykonanie 10 zadań. Zanim wszystkie zadania zostaną uruchomione na ekran zostanie wypisanych kilka napisów Hello World from System.Timers.Timer!. Następnie kiedy wszystkie 10 zadań zostanie uruchomionych napis przestanie się pokazywać. Zobaczymy go ponownie kiedy przynajmniej jedno zadanie zostanie zakończone i tym samym zwolni sie jeden wątek.
class Program
{
  static void Main(string[] args)
  {
    ThreadPool.SetMaxThreads(10, 10);
    
    System.Timers.Timer t = new System.Timers.Timer();
    t.Interval = 1000;
    t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
    t.Start();

    for (int i = 0; i < 10; ++i)
        ThreadPool.QueueUserWorkItem((o) =>
            {
              Console.WriteLine("Start " + o);
              Thread.Sleep(10000);
              Console.WriteLine("End " + o);
            }, i);

    Console.ReadLine();
  }

  static void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    Console.WriteLine("Hello World from System.Timers.Timer!");
  }
}
Taki sam efekt otrzymamy jeśli użyjemy klasy System.Threading.Timer zamiast System.Timers.Timer. Jak można poradzić sobie z tym problemem. Uważam, że są trzy rozwiązania:
  • Zliczać liczbę zadań umieszczanych w puli wątków i pilnować aby zawsze, przynajmniej jeden wątek z puli był wolny i mógł być użyty przez klasę Timer. Maksymalną liczbę wątków możemy pobrać przy pomocy metody GetMaxThreads. Jest to jednak podejście sprzeczne z ideą puli wątków, do której wrzucamy zadania do wykonania i nie zastanawiamy się kiedy dokładnie zostanie uruchomione, przez jaki wątek itd.
  • Zrezygnować z klasy ThreadPool i samemu zarządzać wątkami.
  • Użyć (napisać) zegar odpalający zdarzenia Elapse poza pulą wątków.
W opisie problemu pominąłem Task Parallel Library ponieważ w omawianym przypadku użycie .NET 4.0 nie było możliwe. Szybki test pokazał jednak, że jeśli mógłbym użyć Task Parallel Library to problem nie wystąpiłby. Z tego co wiem Task Parallel Library nie używa klasy ThreadPool, a więc przyczyna problemu nie występuje.

07/05/2010

ShowDialog i zwalnianie zasobów

Home

TestForm form = new TestForm();

if (form.ShowDialog() == DialogResult.OK)
{
  ...
}
Czy powyższy króciutki fragment kodu powodujący wyświetlenie okna dialogowego jest poprawny? Niestety, jeszcze do niedawna powiedziałbym bez mrugnięcia oka, że oczywiście tak. Niestety ponieważ ta odpowiedź jest niepoprawna. Do tego aby ten kod był poprawny brakuje niewielkiej ale za to bardzo istotnej rzeczy i zostało to pokazane poniżej:

using(TestForm form = new TestForm())
{
  if (form.ShowDialog() == DialogResult.OK)
  {
    ...
  }
}
Zamiast klauzuli using można oczywiście wywołać Dispose. Dlaczego jednak jawne zwolnienie zasobów jest w ogóle potrzebne? Czy to oznacza, że za każdym razem kiedy chcemy wyświetlić jakieś okno musimy pamiętać o klauzuli using?

Odpowiedź brzmi nie, jest to konieczne tylko jeśli wyświetlamy okno dialogowe. W takim wypadku po jego zamknięciu nie następuje wywołanie metody Close, która zwolni za nas zasoby. Dzięki temu możemy użyć tego samego okna ponownie. Nie jest to żadna wiedza tajemna i można o tym przeczytać w dokumentacji metody ShowDialog:

Unlike modeless forms, the Close method is not called by the .NET Framework when the user clicks the close form button of a dialog box or sets the value of the DialogResult property.

Kolejny raz przekonuję się, że nawet jeśli czegoś używamy bardzo często i jest to dla nas coś prostego i oczywistego to nie znaczy to, że robimy to na 100% dobrze :)

30/11/2009

Uwierzytelnienie, ASP.NET i IIS

Home

Post ten postanowiłem napisać kiedy zorientowałem się, że kilku kolegów poświęciło sporo czasu aby rozwiązać problem z uwierzytelnieniem usługi sieciowej zainstalowanej na IIS'ie podczas gdy nie posiadali dokładnej wiedzy jak ten mechanizm działa. Problem udało się rozwiązać ale jestem przekonany, że można to było zrobić mniejszym nakładem czasu. Na początku zaznaczę również, że wpis ten dotyczy IIS w wersjach wcześniejszych niż IIS 7.

Jak wszyscy wiemy aplikacje ASP.NET i usługi sieciowe (ang. Web Services) hostowane są na serwerze IIS. Do swojego działania wymagają one dostępu do różnych zasobów. Aby możliwe było określenie czy aplikacja lub usługa posiadają uprawnienie do jakiegoś zasobu muszą one posiadać jakąś tożsamość czyli być uwierzytelnione (ang. authentication). Powstaje pytanie jaką tożsamość posiada aplikacja ASP.NET lub usługa sieciowa?

Na początek przyjrzyjmy się poniższemu scenariuszowi:
  • Użytkownik uruchamia przeglądarkę internetową i wpisuje adres aplikacji.
  • Serwer IIS otrzymuje żądanie i w zależności od ustawień żąda lub nie uwierzytelnienia.
  • Serwer IIS sprawdza czy użytkownik uwierzytelniony (bądź anonimowy) może uzyskać dostęp do żądanego zasobu.
  • Żądanie przekazywane jest do obsługi do silnika ASP.NET.
  • Aplikacja w zależności od ustawień ponownie żąda lub nie uwierzytelnienia się użytkownika.
  • Aplikacja obsługuje żądanie w razie potrzeby legitymując się pewną tożsamością.

Poziom IIS

Na poziomie IIS domyślnie włączone jest uwierzytelnienie anonimowe czyli innymi słowy brak uwierzytelnienia. W takim wypadku zgodnie z domyślnymi ustawieniami przyjmuje się, że użytkownik uwierzytelnił się jako IUSR_machinename. Dostępne są oczywiście inne tryby uwierzytelnienia np.: uwierzytelnienie zintegrowane z Windows (ang. Integrated Windows Authentication) czy uwierzytelnienia podstawowe (ang. Basic) gdzie hasło przekazywane jest czystym tekstem.

Uwierzytelnienia na poziomie IIS jest potrzebne aby stwierdzić czy użytkownik może żądać danego zasobu znajdującego się na serwerze. Zasobem tym może być aplikacja ASP.NET ale również inne rzeczy np.: obrazki, archiwa itd. Uwierzytelnienia na tym poziomie konfigurowne jest przy pomocy aplikacji administratora IIS: Control Panel -> Administrative Tools -> Internet Information Services.

Poziom silnika ASP.NET

Następnie mamy uwierzytelnienia na poziomie silnika ASP.NET. W tym przypadku można włączyć uwierzytelnienie anonimowe, zaimplementować cały mechanizm samemu czy użyć uwierzytelnienia Windows. Należy jednak zaznaczyć, że nie należy mylić uwierzytelnienia Windows na poziomie ASP.NET z uwierzytelnienia zintegrowanym z Windows na poziomie IIS.

Włączenie uwierzytelnienia Windows na poziomie ASP.NET oznacza, że silnik ASP.NET będzie widział użytkownika pod tożsamością wyznaczoną przez IIS. Innymi słowy IIS uwierzytelni użytkownika i przekaże jego tożsamość do ASP.NET. Metoda jaka zostanie użyta przez IIS do uwierzytelnienia zależy od ustawień. W szczególności, tak jak napisałem wyżej, może to być uwierzytelnienia zintegrowane z Windows ale również podstawowe czy inne. W przypadku braku uwierzytelnienia do silnika ASP.NET zostanie natomiast przekazana tożsamość anonimowa. Uwierzytelnienia na tym poziomie konfigurowane jest przy pomocy pliku web.config w sekcji <authentication>.

Tożsamość silnika ASP.NET

Teraz dochodzimy do setna sprawy czyli z jaką tożsamością działa silnik ASP.Net. Odpowiedź: „To zależy” :) Już odpowiadam ale zanim przejdziemy dalej należy jeszcze wyjaśnić czym jest impersonacja. Otóż impersonacja to mechanizm pozwalający przyjąć cudzą tożsamość i wykonywać w jej kontekście różne działania.

A więc jeśli wyłączona jest impersonacja (ustawienia domyślne) to silnik ASP.NET w przypadku systemów Windows Server 2000 i Windows XP działa na koncie o nazwie ASPNET, a przypadku Windows Server 2003 używa konta Network Service. Natomiast jeśli impersonacja jest włączona ASP.NET działa z tożsamością uwierzytelnionego użytkownika nawet jeśli jest to tożsamość anonimowa. Za te kwestie odpowiada sekcja <identity>.

Sprawa wygląda trochę inaczej w przypadku IIS 7, który posiada mocno zmienioną, a właściwie zupełnie inną architekturę w stosunku do IIS 6 i wcześniejszych. W szczególności w IIS 7 nie ma podziału na uwierzytelnienia na poziomie serwera i ASP.NET. Serwer IIS7 może być również konfigurowany z poziomu sekcji <system.WebServer> w pliku web.config. To jednak temat na kolejny wpis.

17/11/2009

SharpPcap

Home

WinPcap to dobrze znane wielu osobom narzędzie umożliwiające przechwytywanie i analizę ruchu sieciowego, jego generowanie oraz wiele innych rzeczy. W oparciu o WinPcap została napisana inna bardzo przydatna aplikacja WireShark. Narzędzi te są bezwątpienia bardzo przydatne ale co jeśli chcemy samemu zaimplementować algorytm analizujący ruch sieciowy. Dodatkowo najlepiej by było zrobić to w C# i nie babrać się w mechanizmach interoperacyjności z kodem niezarządzanym, bawić z atrybutem DllImport itd. Oczywiście chcemy również skorzystać z możliwości WinPcap.

Dokładnie w takiej sytuacji znalazłem się ostatnio. Wypróbowałem kilka rozwiązań ale szeroki uśmiech na mojej twarzy wywołała biblioteka SharpPcap. Została napisana w oparciu o WinPcap ale ukrywa przed użytkownikiem wszelkie odwołania do niezarządzanego kodu. Do tej pory przetestowałem tylko jej podstawowe funkcje ale wydaje się działać bardzo dobrze, a pracuje się z nią przyjemnie.

Zastanawiałem się nad zamieszczeniem tutaj jakiegoś przykładu użycia ale jestem zwolennikiem zasady "Po co robić coś co zostało już zrobione". Zainteresowanych zachęcam więc do pobrania biblioteki i zapoznania się z załączonymi przykładami, które są dobrze skomentowane. .

02/11/2009

Problem z SqlDependency. Czyżby?

Home

Post ten dotyczy mechanizmu query notification, który pozwala na otrzymywanie powiadomień o zmianach w bazie danych dotyczących wybranych wierszy. Funkcjonalność ta jest dostępna na poziomie programistycznym między innymi przez łatwą w użyciu klasę SqlDependency (Jest to opakowanie na klasę SqlNotificationRequest). W Internecie można znaleźć bardzo dużo przykładów użycia tej klasy nie będę, więc powielał tego co zostało już napisane. Chciałbym natomiast zwrócić uwagę, że chociaż warunkiem koniecznym użycia powiadomień jest użycie SQL Server'a w wersji 2005 lub późniejszej to nie jest to warunek wystarczający.

Wszystko zaczęło się od tego, że postanowiłem przyjrzeć się dokładnie temu mechanizmowi. Do testów wybrałem chyba dobrze znana bazę Northwind. Bardzo szybko udało mi się stworzyć testową aplikację ale po jej uruchomieniu okazało się, że powiadomienia nie są generowane albo aplikacja ich nie otrzymuje. Kod sprawdziłem kilka razy, dla pewności przejrzałem kilka opisów w sieci i nic.

W końcu postanowiłem wykorzystać kody pokazane w jednym z tutoriali, wraz z użytą w tam bazą danych. Po chwili okazało się, że działa. Zmodyfikowałem, więc swoją aplikację aby używała właśnie tej bazy danych. Chwila niepewności, uruchamiam i również działa.

Dochodzę do wniosku, że nie ma mocnych, problem musi tkwić gdzieś w bazie danych. Porównuję konfigurację obu baz danych i znajduję winnego - tryb kompatybilności. Okazało się, że po zainstalowaniu baza Northwind ma ustawiany tryb kompatybilności na SQL Server 2000, zamiast SQL Server 2005. Mała, głupia sprawa, a można stracić trochę czasu.

26/10/2009

CodeBehind i CodeFile

Home

Jakiś czas temu pisząc prostą aplikacje WWW utworzyłem z rozpędu projekt typu Web Application zamiast Web Site. Zanim się zorientowałem popełniłem już trochę kodu stwierdziłem więc, że nie będę pisał go od początku. Usunąłem projekt z solution, wykasowałem plik z rozszerzeniem csproj i skorzystałem z polecenia Add -> Existing Web Site.... Wszystko wydawało się w porządku do momentu kiedy spróbowałem skompilować aplikację. W efekcie otrzymałem komunikat jak poniżej:

Could not load type 'PageName'.

Przy drugiej próbie kompilacji otrzymałem taki sam błąd kompilacji. Patrzę i patrzę w kod strony i nic. Przecież jeszcze 5 minut temu kompilowało się, czary? Oczywiście, że nie. Po chwili przypominam sobie o jednej drobnej różnicy. W przypadku projektów typu Web Application w dyrektywie @Page używa się atrybutu CodeBehind, a w przypadku Web Site'ów atrybutu CodeFile. Niby szczegół ale jeśli się o nim zapomni może popsuć trochę krwi.

19/10/2009

Trochę o zwalnianiu zasobów

Home

Każdy dobry programista wie, że po skończeniu pracy z obiektem klasy implementującej interfejs IDisposable należy wywołać metodę Dispose (jawnie bądź nie jawnie). Dlatego kiedy ostatnio zobaczyłem kod, w którym programista beztrosko raz po raz tworzy ikonę, a następnie radośnie o niej zapomina powodując wzrost liczby obiektów GDI przez usta przeszły mi dość niecenzuralne słowa. Oczywiście od razu poprawiłem kod w mniej więcej taki sposób:
using(Icon icon = GetIcon())
{
   ...
}
Nic prostszego można powiedzieć. Jednak przy następnym uruchomieniu aplikacji ku mojemu zdziwieniu liczba obiektów GDI znowu zaczęła rosnąć. Zaglądam, więc do metody GetIcon. A tam widzę coś takiego:
return Icon.FromHandle(handle);
Nie kojarząc za bardzo metody FromHandle zaglądam do dokumentacji, a tam jest napisane coś takiego:

When using this method you must dispose of the resulting icon using the DestroyIcon method in the Win32 API to ensure the resources are released.

Kolejny mój krok to oczywiście sprawdzenie czy wywołanie DestroyIcon działa. Metodę tą należy zadeklarować w następujący sposób:
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet=CharSet.Auto)]
extern static bool DestroyIcon(IntPtr handle);
Jej użycie oczywiście rozwiązało problem. Co dociekliwsi mogą jeszcze zapytać czemu wywołanie Dispose nie wystarcza. Sprawa jest dość ciekawa. Okazuje się, że klasa Icon używa wewnętrznie DestroyIcon do zwalniania zasobów ale tylko i wyłącznie wtedy kiedy jest właścicielem tych zasobów to jest kiedy sama je zaalokuje. W momencie tworzenia ikony przy pomocy uchwytu dostarczonego z zewnątrz trzeba samemu zadbać o jego zwolnienie.

Reasumując w opisanym przypadku dwóch programistów zrobiło dwa poważne błędy. Pierwszy zapomniał o zwolnieniu zasobów, a drugi o dokładnym przeczytaniu dokumentacji. Metoda GetIcon powinna zostać napisana tak aby korzystający z niej programista nie musiał posiadać wiedzy "tajemnej" aby dobrze jej użyć.

11/10/2009

Kontrolki ASP.NET i zdarzenia

Home

Dzisiaj napiszę o rzeczy bardzo prostej ale, o której jednak zdarzyło mi się zapomnieć przez co zmarnowałem trochę czasu. Sytuacja miała miejsce kiedy pracowałem nad kontrolkę, która na swoim interfejsie publicznym między innymi udostępniała zdarzenia SelectionChanged. W kodzie wyglądało to jakoś tak:
...
public event EventHandler SelectionChanged;
...
Po jej napisaniu zabrałem się do testowania i jedną z rzeczy jaką chciałem sprawdzić było to czy zdarzenie jest generowane w odpowiednim momencie. Umieściłem więc kontrolkę na stronie w taki sposób:
...
<cc1:MyControl id="Control1" runat="server" SelectionChanged="Control1_SelectionChanged" />
...
Uruchamiam stronę i coś nie działa. Po krótkiej chwili dochodzę do wniosku, że coś musi być nie tak z zdarzeniem. Stawiam, więc w kodzie pułapkę i odświeżam stronę. Chwila debugu i konsternacja. Kod działa prawidłowo ale w momencie kiedy następuje próba wygenerowania zdarzenia okazuje się, że SelectionChanged równa się null.

Zaczynam sprawdzać czy nigdzie nie zrobiłem literówki itd. Uruchamiam kod ponownie i ciągle to samo. W końcu przypominam sobie, że aby deklaratywnie podczepić się pod zdarzenie kontrolki trzeba użyć przedrostka On. Kod powinien więc wyglądać jak poniżej:
...
<cc1:MyControl id="Control1" runat="server" OnSelectionChanged="Control1_SelectionChanged" />
...