.NET'ten Native Kod Çağırmak: P/Invoke, Marshalling ve LibraryImport
Günümüzde bile, yüksek performanslı hesaplama, donanım erişimi veya mevcut C++ kod tabanlarını kullanmak için .NET uygulamalarının native (C/C++) DLL'leri çağırması gerekir. Platform Invoke (P/Invoke), bu kapıyı açar. Ancak bu kapıdan girerken dikkatli olunmazsa, bellek sızıntıları, erişim ihlalleri (AccessViolation) ve uygulama çökmeleri kaçınılmazdır. Neyse ki .NET 7+ ile gelen LibraryImport Source Generator'ı, bu işlemi hem güvenli hem de performanslı hale getiriyor.
1. P/Invoke Temelleri: Dışarıdan Fonksiyon Çağırmak
Native bir C++ DLL'de şöyle bir fonksiyon olduğunu varsayalım:
cpp
// C++ (native) extern "C" __declspec(dllexport) int AddNumbers(int a, int b);
.NET tarafında bu fonksiyonu çağırmak için:
csharp
// Eski yöntem (DllImport) - HALA ÇALIŞIR
[DllImport("NativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int AddNumbers(int a, int b);
Kritik Noktalar:
-
CallingConvention:
Cdeclmi,StdCallmi,FastCallmi? Windows API'leri geneldeStdCall, C/C++ varsayılanıCdecl'dir. Yanlış seçim stack corruption (yığın bozulması) ile sonuçlanır. -
MarshalAs:
stringgibi yönetilen tipler, nativechar*ile aynı değildir. Varsayılan marshalling, Windows'ta ANSI, Linux'ta UTF-8 olabilir; bu da karakter bozulmasına (mojibake) yol açar.
2. Marshalling (Veri Türü Dönüşümleri) - En Tehlikeli Alan
Aşağıdaki tablo en kritik eşleştirmeleri gösterir:
| .NET Tipi | Native Tip (C/C++) | Açıklama |
|---|---|---|
int, uint |
int, unsigned int |
Güvenli, boyut sabit (4 byte) |
long |
long long (Windows) veya long (Linux) |
TEHLİKELİ! Windows'ta 4, Linux'ta 8 byte olabilir. IntPtr veya long long kullanmak daha güvenli. |
string (Unicode) |
wchar_t* (UTF-16) |
[MarshalAs(UnmanagedType.LPWStr)] ile gelir. |
string (Ansi) |
char* (UTF-8/ANSI) |
[MarshalAs(UnmanagedType.LPStr)] ile. |
byte[] |
unsigned char* |
Genelde direkt pointer geçmek için IntPtr veya Span<byte> tercih edilir. |
struct |
struct / class |
Layout önemlidir. [StructLayout(LayoutKind.Sequential)] ile hizalanmalıdır. |
Örnek - Karmaşık Struct Geçmek:
cpp
// C++
typedef struct {
int id;
char name[128];
double value;
} MyData;
extern "C" __declspec(dllexport) void ProcessData(MyData* data);
csharp
// .NET
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct MyData
{
public int id;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string name;
public double value;
}
[DllImport("NativeLibrary.dll")]
public static extern void ProcessData(ref MyData data);
⚠️ Tuzak: bool tipi native BOOL (4 byte) ile eşleşirken, .NET'te bool 1 byte'tır. [MarshalAs(UnmanagedType.Bool)] veya [MarshalAs(UnmanagedType.I4)] ile netleştirilmezse, değerler yanlış okunur.
3. Bellek Yönetimi: Kim Serbest Bırakacak?
Native kod, malloc ile bir bellek tahsis edip bunu .NET'e döndürürse, .NET bu belleği free ile serbest bırakamaz (çünkü farklı bir heap yöneticisi). Bu durumda bellek sızıntısı oluşur.
-
Çözüm: Native DLL'de ayrıca
FreeMemorygibi bir fonksiyon sağlanmalı ve .NET, işi bittiğinde bu fonksiyonu çağırmalıdır. -
Alternatif:
CoTaskMemAlloc/CoTaskMemFreegibi Windows ortak (COM) tahsis yöntemlerini kullanmak, çünkü .NETMarshal.FreeCoTaskMemile bunları serbest bırakabilir.
En Güvenli Yöntem: Native tarafa bir buffer (önceden ayrılmış byte[]) gönderip, sonucu bu buffer'a yazdırmak. Böylece tahsis ve serbest bırakma tamamen .NET tarafında kalır.
4. Performans Riskleri ve P/Invoke'un Maliyeti
Her P/Invoke çağrısı, yönetilen-yönetilmeyen (managed-unmanaged) sınırı geçer. Bu sınırın maliyeti vardır:
-
Sınır Geçişi (Transition): Yaklaşık 50-100 ns (basit çağrı) – çok küçüktür.
-
Marshalling: Karmaşık veri yapıları (string, struct) kopyalanır. Bu, çağrı başına milisaniyelere çıkabilir.
-
Pinvoke çağrıları JIT optimizasyonlarını engelleyebilir.
Optimizasyon Taktikleri:
-
Sık çağrılan fonksiyonları
[SuppressGCTransition]ile işaretleyerek GC'nin çağrı sırasında thread'i durdurmasını engelleyin (tehlikeli, sadece çok hızlı fonksiyonlar için). -
Büyük veri akışları için
byte[]veyaSpan<byte>kullanarak toplu marshalling yapın. -
Native kodda karmaşık işlemler yapıyorsanız, mümkünse birkaç küçük çağrı yerine tek bir toplu çağrı (batch) yapın.
5. LibraryImport: Eski DllImport'u Unutun! (Source Generator Yaklaşımı)
.NET 7 ile gelen LibraryImport, derleme zamanında P/Invoke kodunu üreterek çalışma zamanı marshalling maliyetini ortadan kaldırır ve AOT (Native AOT) ile uyumludur.
csharp
using System.Runtime.InteropServices;
public partial class NativeMethods
{
// LibraryImport ile, DllImport'ten çok daha hızlı ve güvenli
[LibraryImport("NativeLibrary.dll",
EntryPoint = "AddNumbers",
CallingConvention = CallingConvention.Cdecl)]
public static partial int AddNumbers(int a, int b);
// String ile - StringMarshalling ile karakter setini belirt
[LibraryImport("NativeLibrary.dll",
EntryPoint = "PrintMessage",
StringMarshalling = StringMarshalling.Utf8)]
public static partial void PrintMessage(string message);
// Struct ile - In/Out yönünü belirt
[LibraryImport("NativeLibrary.dll")]
public static partial void ProcessData(ref MyData data);
}
LibraryImport'ın Avantajları:
-
Runtime Reflection Yok: Marshalling kodları derleme anında üretilir, bu da başlangıç (startup) hızını artırır.
-
Native AOT Dostu:
DllImportAOT derlemesinde çalışmazken,LibraryImportsorunsuz çalışır (iOS, WebAssembly, Linux musl gibi ortamlar). -
Otomatik Boyut Kontrolü: Marshalling sırasında oluşan hatalar derleme zamanında yakalanır (ör. yanlış
charboyutu).
Ancak Dikkat: LibraryImport, DllImport'un tüm özelliklerini (ör. SetLastError = true) desteklemez. Bazı eski Windows API'leri için DllImport hala gereklidir.
6. Gerçek Dünya Önerileri
-
Kritik Sistemler İçin: Native kodu bir wrapper (C++/CLI) ile sarmalayıp, .NET'e yönetilen bir arayüz sunabilirsiniz. Bu, marshalling hatalarını azaltır.
-
Hata Yönetimi: Native kod
SetLastErrorveyaerrnoayarlar.Marshal.GetLastWin32Error()ile hata kodunu alıp anlamlı exception'lar fırlatın. -
Test: P/Invoke çağrılarını birim test ile değil, entegrasyon testi ile sınayın çünkü DLL yokluğu veya platform farkı (x86/x64) test ortamında patlayabilir.
Sonuç:
P/Invoke, .NET'in süper gücüdür; ancak "güç" beraberinde "sorumluluk" getirir. Yeni projelerde LibraryImport kullanarak marshalling'ı derleme zamanına taşıyın, struct layout'larını netleştirin ve bellek yönetiminin kimin sorumluluğunda olduğunu asla unutmayın. Unutmayın, yanlış bir pointer işlemi, .NET'in güvenli dünyasını tamamen çökertir.