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.2/infersharp-linux64-v1.2.tar.gz
tar -xvzf infersharp-linux64-v1.2.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.