Bármilyen programban programnyelvtől függetlenül a legnehezebb megtalálni a versenyhelyzet és memóriaszivárgásos hibákat. Ezek egy része felfedezhető a kód olvasásával, mások megtalálása azonban több órányi, rosszabb esetben akár több napnyi hibakeresést von magával. Ez mondanom sem kell, hogy lélekölő.
A sok napnyi kimerÃtÅ‘ debuggolás ihlette meg az Infer (https://fbinfer.com/) mögött álló Meta (korábbi néven Facebook) fejlesztÅ‘ csapatot. Az Infer egy moduláris statikus kódanalÃzis eszköz kifejezetten versenyhelyzet és memóriaszivárgások megtalálására. Eredetileg C/C++, Objective-C és Java nyelveket támogatott, de a .NET csapat bÅ‘vÃtette egy C# réteggel és az új projekt neve Infer# lett.
Az Infer# az eredeti Infer felépÃtésébÅ‘l adódóan egy Linux-ra szánt eszköz. Ez két következményt von magával. Az elsÅ‘ következmény, hogy csak .NET Core projektek esetén használhatjuk. A másik következmény pedig az, hogy ha Windows-on használni szeretnénk, akkor WSL2 futtatásra képes számÃtógépre és Windows 10 vagy 11 verzióra lesz szükségünk.
A programot telepÃteni a következÅ‘ módon tudjuk az aktuális mappába:
wget https://github.com/microsoft/infersharp/releases/download/v1.5/infersharp-linux64-v1.5.tar.gz
tar -xvzf infersharp-linux64-v1.5.tar.gz
Ez egy infersharp nevű mappába kicsomagolja a programot, amit aztán a lefordÃtott binárisokon tudunk futtatni. Például ha a kódunk a /mnt/c/program mappában helyezkedik el, mi pedig az infersharp mappában vagyunk, akkor a következÅ‘ módon tudjuk futtatni az analÃzist:
./run_infersharp.sh /mnt/c/program/bin
Milyen hibákat tud megtalálni?
Az elsÅ‘ hibatÃpus, amit képes megtalálni az a null referencia hibák. Ezek kivédésére vezették be a Nullable reference types funkciót, de ha egy örökölt kódbázisunk van, akkor ennek a bekapcsolása és a kód átÃrása a sok munkától a nem éri meg spektrumon belÅ‘l bárhol elhelyezkedhet.
Nézzünk egy példát:
internal class NullObj
{
internal string Value { get; set; }
}
class Program
{
private static NullObj ReturnNull()
{
return null;
}
static void Main(string[]) args)
{
var returnNull = ReturnNull();
_ = returnNull.Value;
}
}
Erre a kódra futtatva az elemzőt szólni fog, hogy a returnNull változó nincs null ellenőrizve, mielőtt használnánk. A kimenet valami hasonló lesz:
Program.cs:11: error: NULL_DEREFERENCE (biabduction/Rearrange.ml:1622:55-62:)
pointer 'returnNull' could be null and is dereferenced at line 11, column 13.
A második tÃpusú hiba, amit meg tud találni, az a nem megfelelÅ‘ erÅ‘forrás felszabadÃtáshoz kapcsolódik. Ha egy objektum implementálja az IDisposable interfészt, akkor nekünk kell gondoskodnunk a megfelelÅ‘ felszabadÃtásról. Ha ezt elmulasztjuk, akkor memóriaszivárgást viszünk a rendszerbe. Nézzünk egy példát:
public StreamWriter AllocatedStreamWriter()
{
FileStream fs = File.Create("everwhat.txt");
return new StreamWriter(fs);
}
public void ResourceLeakBad()
{
StreamWriter stream = AllocateStreamWriter();
}
Jelen esetben a hiba az lesz, hogy a ResourceLeakBad metódusban a létrejövÅ‘ StreamWriter nem kerül felszabadÃtásra, vagyis a hozzátársÃtott everwhat.txt állomány egészen addig nem olvasható, amÃg a programunk be nem záródik. Erre futtatva az elemzést valami ilyesmi kimenetet fogunk kapni:
Program.cs:11: error: RESOURCE_LEAK
Leaked { %0 -> 1 } resource(s) at type(s) System.IO.StreamWriter.
Az ilyen hibák megtalálását nehezÃti a try-catch blokk, mivel ez lényegében egy szofisztikált ugró utasÃtás. Azonban az Infer# legújabb verziója ezekkel is elboldogul:
public void ResourceLeakExcepHandlingBad()
{
StreamWriter stream = AllocateStreamWriter();
try
{
stream.WriteLine(12);
}
catch
{
Console.WriteLine("Fail to write");
}
finally
{
// Finally
}
}
A fenti kódra futtatva szintén megkapjuk a Leaked { %0 -> 1 } resource(s) at type(s) System.IO.StreamWriter. üzenetet a megfelelő sorral.
A harmadik tÃpusú hiba, amivel elboldogul az eszköz, az a versenyhelyzet detektálás. Nézzünk egy példaprogramot:
public class RaceCondition
{
private readonly object _object = new object();
public void TestMethod()
{
int FirstLocal;
FirstLocal = TestClass.StaticIntegerField;
}
public void FieldWrite()
{
lock (_object)
{
{
TestClass.StaticIntegerField = 1;
}
}
}
}
A kód megértéséhez tételezzük fel, hogy a TestClass.StaticIntegerField egy int tÃpusú statikus mezÅ‘je a TestClass osztálynak. Továbbá tételezzük fel, hogy a TestMethod() és a FieldWrite() metódusok két külön szálon futnak. Ebben az esetben a TestMethod() olvasáskor nem feltétlen azt fogja visszaadni, mint amit a FieldWrite() metódus beállÃt. Ennek oka az, hogy nem garantálható jelen programban, hogy melyik fér éppen hozzá. Erre futtatva az elemzést szintén megkapjuk ezt hibaként:
warning: Thread Safety Violation
Read/Write race. Non-private method `RaceCondition.TestMethod()` reads without synchronization from `Assets.TestClass.Cilsil.Test.Assets.TestClass.StaticIntegerField`. Potentially races with write in method `RaceCondition.FieldWrite()`.
Reporting because another access to the same memory occurs on a background thread, although this access may not.
Összességében az Infer# egy nagyon hasznos eszköz makacs és amúgy nehezen megtalálható hibák felderÃtésére. Tudása folyamatosan fejlÅ‘dik. A legfrissebb változata a https://github.com/microsoft/infersharp oldalról szerezhetÅ‘ be. A könyv Mellékletek szekciójában található egy leÃrás a WSL2 engedélyezésérÅ‘l Windows10 és 11 rendszerek esetén.