A .NET 7 és a .NET 8 legnagyobb újdonsága a natÃv kód generálásának lehetÅ‘sége a publish folyamat során. Ennek elÅ‘nye, hogy az Ãgy közzétett alkalmazásaink futtatásához nem kell a .NET-et telepÃtenünk és nem is kell az alkalmazásunk mellé csomagolni. Ez kisebb méretű és gyorsabban induló alkalmazásokat eredményez. Azonban ennek megvannak a limitációi:
- A dinamikus szerelvénybetöltés nem támogatott
- Nincs lehetőség futás idejű kód generálásra a
System.Reflection.EmitAPI segÃtségével - A C++/CLI szerelvények és a COM nem támogatott
- A nem használt kódrészletek el lesznek távolÃtva (Trimming), ami limitációkkal jár:
- A COM marshalling nem támogatott, mivel csak futásidőben eldönthető, hogy mire lenne pontosan szükség.
- WPF nem támogatott, mivel erÅ‘sen épÃt a Reflection alapú működésre és csak futási idÅ‘ben dÅ‘l el, hogy pontosan melyik belsÅ‘ metódusok lesznek meghÃvva.
- Windows Forms nem támogatott, mivel erÅ‘sen épÃt a COM marshalling-ra.
- A teljes alkalmazás egy .exe fájlba fordul, ami eltérő működést eredményez egyes API-k működésében.
- Jelenleg nem minden .NET osztálykönyvtár Native AOT kompatibilis.
ASP.NET esetén is van pár limitáció jelenleg. EzekrÅ‘l bÅ‘vebb információt a https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot és a https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot cÃmen olvashatunk.
NatÃv fordÃtás
A natÃv fordÃtás a publish folyamat részeként, a dotnet publish parancs segÃtségével történik. NatÃv binárisokat akkor fogunk kapni, ha a projektünkben egy PropertyGroup elemen belül engedélyezve van az AOT a PublishAot elemmel:
<PropertyGroup>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
Az IsAotCompatible attribútum megadása nem kötelezÅ‘, de érdemes megadni, mivel ez speciális kód analizátorokat aktivál, amelyek segÃtenek AOT kompatibilissá tenni a kódunkat.
NatÃv közzétételhez a publish parancs után meg kell adnunk egy futtatókörnyezet azonosÃtót, amit célozni szeretnénk:
dotnet publish -r win-x64 -c Release
dotnet publish -r linux-arm64 -c Release
dotnet publish -r linux-x64 -c Release
NatÃv osztálykönyvárak
A natÃv közzététellel nem csak alkalmazásokat készÃthetünk, hanem lehetÅ‘ségünk van natÃv, dinamikusan betöltött könyvtárak (DLL) létrehozására is, ami lehetÅ‘séget ad arra, hogy C/C++-ban Ãrt alkalmazásokba töltsünk be .NET-ben Ãrt kódot.
NatÃv módon metódusokat tudunk közzétenni, amelyek megfelelnek két követelménynek: a metódusnak statikusnak kell lennie és a visszatérési értéke, valamint a bemeneti paraméterei érték tÃpusok (struct). Ezen felül annotálni kell az exportálni kÃvánt metódusokat az UnmanagedCallersOnly attribútummal. Ennek az attribútumnak az EntryPoint tulajdonságával tudjuk megadni az exportált függvény nevét, ami alapján majd meghÃvható lesz.
Az exportált metódusokat csak kÃvülrÅ‘l van lehetÅ‘ség a továbbiakban meghÃvni. Ha menedzselt kódból akarnánk meghÃvni egy ilyen metódust, akkor kivételt kapunk.
Fontos megjegyezni, hogy az exportált metódusok esetén, ha felmerülhet annak az esélye, hogy kivétel keletkezik, akkor azt kapjuk el a metódusban. Erre azért van szükség, mert a natÃv függvény könyvtárak alacsony szintű, C-szerű API-t valósÃtanak meg és a C nem rendelkezik a kivételkezelés koncepciójával. Ez azt jelenti, hogy ha kivétel keletkezne, amit mi nem kaptunk el, akkor az könnyen magával ránthatja a hÃvó alkalmazást, ami minimum kellemetlenséget, rosszabb esetben azonban biztonsági rést is jelenthet.
Felmerülhet a kérdés, hogy akkor mégis hogy jelezzük, hogy ha valami félrement a metódusunkban? A függvény visszatérési értékével. Ha hibára szaladhat a függvényünk kivétel miatt, akkor például egy int visszatérési értékkel jelezhetjük ezt, mégpedig úgy, hogy ha minden sikeres volt, akkor 0 értéket adunk vissza, hiba esetén pedig nem 0 értéket.1
Az alábbi példa szemlélteti a natÃv metódus exportálás lehetÅ‘ségeit:
using System.Runtime.InteropServices;
namespace NatvieClasslib;
public static class NativeLib
{
[UnmanagedCallersOnly(EntryPoint = "factorial")]
public static long Factorial(int n)
{
long result = 1;
for (int i=1; i<=n; i++)
{
result *= i;
}
return result;
}
[UnmanagedCallersOnly(EntryPoint = "hello")]
public static int Hello(IntPtr namePtr)
{
try
{
string? name = Marshal.PtrToStringUni(namePtr) ?? "Unknown";
Console.WriteLine($"Hello, {name}!");
return 0;
}
catch
{
//hiba esetére hibakód.
return -1;
}
}
[UnmanagedCallersOnly(EntryPoint = "hello_str")]
public static IntPtr HelloString(IntPtr namePtr)
{
string? name = Marshal.PtrToStringUni(namePtr) ?? "Unknown";
string helloName = $"Hello, {name}!";
return Marshal.StringToCoTaskMemUni(helloName);
}
}
Egyszerű tÃpusok esetén semmi extra teendÅ‘nk nincs, mint ahogy az látható a Factorial metódus esetén. Azonban ha szövegekkel dolgozunk, akkor azokat mutatóként, IntPtr formában tudjuk fogadni, amibÅ‘l string tÃpust a Marshal osztály PtrToStringUni metódusával tudunk csinálni Unicode (wchar_t) karakterekbÅ‘l álló szöveg esetén. Ha pedig ANSI szöveget szeretnénk feldolgozni, akkor a PtrToStringAnsi metódust kell használnunk.
A HelloString metódusban látható, hogy a feldolgozott szövegünket is vissza kell konvertálnunk egy mutatóra. Ebben szintén a Marshal osztály lesz segÃtségünkre a StringToCoTaskMemUni és StringToCoTaskMemAnsi metódusaival Unicode és ANSI szövegekhez.
A natÃv fordÃtáshoz az alábbi parancsot kell kiadnunk:
dotnet publish -r win-x64 /p:NativeLib=Shared
Ez egy 64 bites Windows DLL fájlt produkál. ElérÅ‘ runtime megadásával (pl. linux-x64) a platformnak megfelelÅ‘ dinamikus könyvtárat kapunk. Linux esetén egy .SO fájlt, Mac esetén pedig egy .DYLIB kiterjesztésűt. Ezt aztán egy natÃv, például C vagy C++ kódunkból a platform specifikus API hÃvásai segÃtségével meg tudunk majd hÃvni. Az alábbi példakód ezt mutatja be Windows és C++ esetén:
#include <iostream>
#include "windows.h"
#include <io.h>
//Ole32dll függőség, a CoTaskMemFree miatt.
//A #pragma lib utasÃtásokat csak a Visual studio támogatja
//mingw alatt: -L. -lole32 argumentumokkal fordÃtani
#pragma comment (lib, "ole32.lib")
#define LIB_PATH L"natvieclasslib.dll"
long callFactorial(int n);
void callHello(wchar_t *name);
wchar_t *callHeloStr(wchar_t * name);
HINSTANCE dllHandle;
int main()
{
//Ellenőrizzük, hogy a fájl létezik-e
if (_waccess(LIB_PATH, F_OK) == -1)
{
std::cerr << "missing natvieclasslib.dll";
return -1;
}
//DLL betöltése
dllHandle = LoadLibraryW(LIB_PATH);
//factorial hÃvás
long fib = callFactorial(5);
std::wcout << L"factorial(5): " << fib << std::endl;
//hello hÃvás
std::wcout << L"hello(\"Teszt Elek\"):" << std::endl;
callHello(L"Teszt Elek");
//hello_str hÃvás
std::wcout << L"hello_str(\"Gipsz Jakab\"):" << std::endl;
wchar_t *result = callHeloStr(L"Gipsz Jakab");
std::wcout << result << std::endl;
//result buffer felszabadÃtása
CoTaskMemFree(result);
//DLL felszabadÃtása
FreeLibrary(dllHandle);
}
long callFactorial(int n)
{
typedef long(*myFunc)(int);
myFunc factorialImport = (myFunc)GetProcAddress(dllHandle, "factorial");
return factorialImport(n);
}
void callHello(wchar_t *name)
{
typedef int(*myFunc)(wchar_t*);
myFunc helloImport = (myFunc)GetProcAddress(dllHandle, "hello");
helloImport(name);
}
wchar_t *callHeloStr(wchar_t * name)
{
typedef wchar_t*(*myFunc)(wchar_t*);
myFunc helloStrImport = (myFunc)GetProcAddress(dllHandle, "hello_str");
return helloStrImport(name);
}
Windows esetén a LoadLibraryW hÃvással tudunk betölteni egy DLL fájlt, amit majd a FreeLibrary hÃvással a program végén el kell engednünk. A betöltött fájlból a meghÃvandó függvény memória cÃmét a GetProcAddress segÃtségével tudjuk megkeresni. A visszakapott cÃmet konvertálnunk kell egy megfelelÅ‘ tÃpusokkal rendelkezÅ‘ függvény pointerré, amit majd meg tudunk hÃvni. A .NET kódunkban allokált szövegeket (HelloString metódus eredménye a példában) a CoTaskMemFree hÃvással tudjuk felszabadÃtani, amihez az ole32 könyvtárat linkelni kell az alkalmazásunkhoz. Ezt Visual C-al fordÃtva a #pragma comment (lib, "ole32.lib") elvégzi, de GCC/G fordÃtás esetén errÅ‘l fordÃtás közben a -L. -lole32 kapcsolók megadásával kell gondoskodnunk.
A C program kimenete:
factorial(5): 120
hello("Teszt Elek"):
Hello, Teszt Elek!
hello_str("Gipsz Jakab"):
Hello, Gipsz Jakab!
-
Ennek oka az, hogy a C nem támogatta a
booltÃpust egészen a C99 szabványig és az ANSI C még most sem támogatja. A kompatibilitás maximalizálása érdekében ezért még mindigintértéket visszaadni, ahol a 0 jelenti azt, hogy minden rendben volt. A fordÃtott logika annak az oka, hogy C eseténifutasÃtásokban szerepelhet egész szám is és a nullától eltérÅ‘ érték igaz értékként van kezelve.↩