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.↩