Node.js Addon’larını .NET Native AOT ile Yazmak
Bir şey dikkatimi çekti: Geçen ay bir müşterimizin VS Code uzantısı üzerinde çalışırken tuhaf bir şey çıktı karşıma. Uzantı, Windows Registry’den veri okuyacaktı ve eldeki çözüm — tahmin edin ne — C++ ile yazılmış bir native addon’dı; node-gyp ile derleniyor, Python istiyor, CI pipeline’ını da gereksiz yere uzatıyordu… Hani şu “çalışıyor ama dokunma” dediğimiz tipik durumlardan biri işte. Tam o sırada Microsoft’un C# Dev Kit ekibinin benzer bir sıkıntıyı Native AOT ile nasıl çözdüğünü okudum ve içimden “bu, bizim derdin tam göbeği” dedim.
Şöyle söyleyeyim, Bakın şimdi, olay sadece “C++ yerine C# yazalım” değil. Asıl mesele, mühendislik ekibinin günlük akışını hafifletmek, CI süresini kısaltmak ve yeni gelen birinin ilk günden elini kirletmeden işe yarar hâle gelmesini sağlamak (ki bu kısım bazen koddan daha pahalıya patlıyor). Gelin bunu biraz eşeleyelim; çünkü yüzeyde basit görünen şeyin altında baya iş gören birkaç detay var.
Bunu biraz açayım.
node-gyp Derdi: Neden Değişiklik Gerekiyor?
Bi saniye — Node.js tarafında native addon yazmaya kalkınca, iş dönüp dolaşıp node-gyp’e geliyor (buna dikkat edin). C ya da C++ ile bir shared library yazıyorsunuz, sonra node-gyp onu derlemeye çalışıyor. Tam o anda küçük görünen ama (söylemesi ayıp) can sıkan zincir başlıyor. Önce Python lazım. Ama öyle her Python da olmuyor; çoğu zaman eski bir sürüm istiyor. Windows kullanıyorsanız Visual Studio Build Tools da gerekiyor. Linux’ta gcc, macOS’ta Xcode Command Line Tools… Kısacası, “sadece paket kurayım” diye giriyorsunuz, bir bakmışsınız derleyici avına çıkmışsınız.
Ve işler burada ilginçleşiyor.
Açıkçası, 2021’de Logosoft’ta Node.js tabanlı bir izleme aracı geliştirirken ben de aynı duvara tosladım. Ekip 4 kişiydi, herkesin makinesinde başka bir Python vardı; birinde 3.11, diğerinde 2.7, ötekinde hiç yok. CI tarafı Actions" data-glossary-term="GitHub Actions">GitHub Actions üzerindeydi ve her build’de Python kurulumu ile node-gyp derlemesi toplamda yaklaşık üç dakika yutuyordu. Üç dakika az gibi duruyor, biliyorum, ama günde 15-20 build yapan ekipte bu iş ay sonunda 15-20 saate vuruyor (ve açık konuşayım, insanın sınırını da yiyor). Peki neden böyle bir yükü taşıyalım?
C# Dev Kit ekibi de aynı dertle uğraşmış. Ekipte.NET SDK zaten var,.NET araçları da hazır; ama native addon tarafına gelince bir anda Python’a ve C++ toolchain peşine düşmek zorunda kalıyorlar (yanlış duymadınız). Garip değil mi? Elinizde.NET varken neden ayrıca C++ ortamı kurasınız ki? İşin aslı tam burada değişiyor zaten.
Native AOT Burada Devreye Giriyor
Dur bir saniye, önce şunu netleştireyim: Native AOT ne yapıyor? C# kodunu alıp doğrudan platforma özel native binary’ye çeviriyor; yani ortada CLR yok, JIT yok, arada dolaşan ekstra bir katman da pek kalmıyor. Sonuçta elinizde C’den çağrılabilen bir shared library (.dll,.so,.dylib) oluyor (inanın bana)
İşin garibi, Node.js addon’ları ne istiyor peki? Bir shared library ve napi_register_module_v1 diye bir giriş noktası. İşin hoş tarafı şu: N-API, kütüphaneyi hangi dille yazdığınızla ilgilenmiyor; doğru sembolleri export ediyor musunuz ona bakıyor. Biraz kuru bir dünya ama iş görüyor. Native AOT da tam burada devreye girip bu ihtiyacı karşılayabiliyor.
Açık konuşayım: Hmm, bir saniye daha… Aslında bu yaklaşımın olayı şu: N-API zaten ABI-stable bir C API’si; yani Node.js sürümü değişse bile addon’unuzun ayağı çok kolay kaymıyor. Native AOT ile ürettiğiniz shared library de aynı mantıkta stabil kalıyor (tabii her şeyin sihirli biçimde sorunsuz olacağını da sanmayın), iki taraf da “bana C seviyesinde bir kapı ver, gerisini ben hallederim” diyor.
Hmm, bunu nasıl anlatsam…
Proje Dosyası — Sadeliğin Güzelliği
Beni en çok şaşırtan şey proje dosyasının ne kadar kısa kaldığıydı. Bakın:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PublishAot>true</PublishAot>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
Üç satır gibi duruyor, evet. PublishAot ile “bana shared library üret” diyorsunuz; AllowUnsafeBlocks ise N-API interop sırasında gereken function pointer ve fixed buffer işleri için lazım oluyor (buna dikkat edin). Python yok, node-gyp yok, garip bağımlılık zinciri yok; açık konuşayım, bu taraf baya rahatlatıyor.
Bence böyle.
Evet.
Kodun İçine Dalalım: Entry Point ve Fonksiyon Kaydı
Node.js shared library’yi yükleyince ilk iş olarak napi_register_module_v1 fonksiyonunu çağırıyor. C# tarafında bunu [UnmanagedCallersOnly] ile bağlıyorsunuz; yani giriş noktası baya net oluyor ama işin içinde yine de küçük bir sürpriz var çünkü imza doğru değilse tüm zincir sessizce bozulabiliyor:
public static unsafe partial class RegistryAddon
{
[UnmanagedCallersOnly(
EntryPoint = "napi_register_module_v1",
CallConvs = [typeof(CallConvCdecl)])]
public static nint Init(nint env, nint exports)
{
Initialize();
RegisterFunction(
env,
exports,
"readStringValue"u8,
&ReadStringValue);
return exports;
}
}
Burada "readStringValue"u8 kullanılmış. UTF-8 string literal.NET 7 ile gelen bu detay N-API’nın beklediği encoding ile uyuyor; fena da çalışmıyor açıkçası. JavaScript tarafından çağırınca modül sanki yerleşikmiş gibi davranıyor. Peki neden önemli? Çünkü benim tarafta ufak bir kayma bile olursa sonra dönüp saç baş yoluyorsunuz.
RegisterFunction Detayları
Aslında fonksiyon kaydı kısmı N-API’nın napi_define_properties ya da napi_set_named_property çağrılarını sarıp sarmalıyor. C#’tan bu C fonksiyonlarına genelde DllImport ile gidiyorsunuz ama LibraryImport da iş görüyor; şey, burada asıl mesele çağrıyı yapmak değil, dönen sonucu ciddiye almak. N-API fonksiyonları napi_status veriyor ve her seferinde kontrol etmeniz gerekiyor. İlk denememde bunu ben de es geçmiştim — addon yükleniyordu ama fonksiyonlar undefined geliyordu; iki saat debug sonrası anladım ki napi_set_named_property çağrısında parametre sırasını ters çevirmişim (evet, böyle basit bir şey yüzünden). Hata kodu oradaydı ama ben bakmamıştım. Ders alındı.
Evet. Bu konuyla ilgili Foundry Agent’a MCP ile Özel Araç Bağlamak yazımıza da göz atmanızı tavsiye ederim.
Türkiye’deki Ekipler İçin Ne Anlam İfade Ediyor?
Şimdi asıl meseleye gelelim. Kurumsal müşterilerde şunu sık görüyorum: (söylemesi ayıp) Türkiye’de Node.js + native addon kullanan proje sayısı az değil, hatta bazen insanın beklediğinden fazla çıkıyor; özellikle fintech tarafında, e-ticaret işlerinde. Kurumsal dashboard uygulamalarında bu iş dönüp dolaşıp Windows servislerine, Registry’ye ya da platform-specific API’lere dayanıyor.
Birkaç ay önce buna çok benzeyen bir senaryo yaşadık (buna dikkat edin). Node.js tabanlı bir dashboard uygulaması Windows Certificate Store’dan sertifika okumak için C++ addon kullanıyordu; her deployment’ta node-gyp derlemesi çalışıyor, sonra bir bakıyorsunuz Visual Studio Build Tools güncellemesi gelmiş ve build patlamış oluyor. Ekip 6 kişiydi, 2 kişi.NET biliyordu ama C++ bilen yoktu; peki addon’da bug çıksa kim düzeltecek? — bence çok yerinde bir karar —
Bir dakika — bununla bitmedi.
Eğer ekibiniz zaten.NET biliyorsa, native addon’ları C++ yerine C# Native AOT ile yazmak sadece teknik bir tercih değil — ekibin sahiplenebileceği, bakım yapabileceği kod üretmek demek.
Ha bu arada küçük ekipseniz ya da startup tarafındaysanız tablo biraz değişiyor. Neden önemli bu? Eğer zaten Node.js + TypeScript ortamında rahat ediyorsanız. Native addon’a gerçekten ihtiyacınız yoksa açık konuşayım bu konuya girmenize pek gerek yok; ama platform-specific bir ihtiyaç varsa ve ekipte.NET bilen biri duruyorsa değerlendirmeye değer (bu beni çok şaşırttı).
Avantaj ve Dezavantaj Karşılaştırması
Lafı gevelemeden bir tabloyla özetleyeyim:
| Kriter | C++ (node-gyp) | C# (Native AOT) |
|---|---|---|
| Build bağımlılıkları | Python değil tabii ki Python tam adıyla birlikte C++ toolchain ve node-gyp gerekir | Sadece.NET SDK yeterli olur |
| Bu satır biraz garip duruyor ama tabloyu bozmayalım. | ||
| Düzeltme notu: | C++ toolchain + Python + node-gyp bağımlılığı vardır. | .NET SDK ile publish edilir. |
| Neyse uzatmayalım. | ||
| Cİ/CD süresi | Daha uzun sürer | AOT publish süresi vardır ama akış daha temizdir |
| Yeni geliştirici onboarding | Zor — birçok araç kurulumu gerekir | Daha kolay -.NET SDK çoğu durumda yeterli |
| C# debugging daha rahat ilerler | ||
| Küçük kalırDaha büyük (~5-15 MB) olur | Küçük kalır | Daha büyük (~5-15 MB) olur |
Hani, Evet,’işin özeti bu kadar basit değil’. C++ tarafı hafif geliyor tamam; ama o hafiflik bazen insanın başına iş açıyor çünkü Python toolchain ve node-gyp derken kurulum zinciri uzuyor. Ekipte yeni biri varsa ilk gün biraz sürünüyor.
Daha açık söyleyeyim, bi saniye — C# Native AOT tarafında ise tablo tersine dönüyor gibi düşünün. Build daha temiz hissettiriyor olabilir (belki yanılıyorum ama), debug tarafı da daha rahat ilerliyor; fakat çıkış dosyası büyüyor çünkü runtime’ı gömüyorsunuz. Bu arada ekosistem farkı da var; mesela Registry ya da Crypto gibi.NET dünyasında hazır gelen parçalarla uğraşmak baya iş görüyor.Kubernetes AI Gateway WG: AI Trafiği Artık Standart.
Binary boyutu konusunda açık konuşayım: Native AOT çıktısı C++ ile karşılaştırıldığında büyük olurdu diyeyim daha doğru olur sanırım.Bir Registry okuma addon’u için C++ ile belki 200 KB’lık bir.dll çıkarsınız,Native AOT ile bu 8-10 MB olabilir.Ama 2025’te 10 MB’lık dosya sorun mu?Çoğu durumda değil.Yine de edge case’lerde — mesela IoT cihazları veya çok kısıtlı ortamlar — bu fark önemli olabilir.AI Maliyet Optimizasyonu: ROI’yi Gerçekten Artırmanın Yolu. Foundry Local GA Öldü: Bulut Olmadan Yerel AI.
Bunu biraz açayım.
Kısa not düşeyim buraya.SQL MCP Server: Veritabanını Ajanlara Açmanın Yolu.
Peki neden? Çünkü burada seçim sadece “küçük dosya” seçimi değil; bakım yükü, ekip alışkanlığı ve dağıtım rahatlığı da devreye giriyor. Açıkçası ben çoğu senaryoda C# tarafını daha az yorucu buluyorum,ama bazı dar alanlarda C++ hâlâ kendini kurtarıyor.
Nereden Başlamalı?
Sade Bir “Hello World” Addon Yazın İlk Olarak!
Bak şimdi,bu işe devasa bir şeyle girişmeyin.Küçük başlayın.Önce basit addon yazın;JavaScript’ten бip fonksiyon çağırın,sadece string dönsün.Çalıştığını görünce gerisi. Daha rahat geliyor,yoksa ilk günden her şeyi aynı anda çözmeye çalışınca insan biraz dağılıyor (evet, doğru duydunuz)
- .NET 9 veya 10 SDK’yı yükleyin (net10.0 hedefliyorsanız preview gerekebilir)
- Yukarıdaki minimal proje dosyasını oluşturun
- >Entry point fonksiyonunu yazın<
>dotnet publish -r win-x64 -c Release<- >Çıkan.dll’i Node.js’te <
> ile yükleyin
Sıkça Sorulan Sorular
Native AOT ile derlenen addon her platformda çalışıyor mu?
Hayır, maalesef çalışmıyor. Her platform için ayrı ayrı publish yapman gerekiyor çünkü cross-compilation şu an desteklenmiyor. Aslında en pratik çözüm CI/CD tarafında matrix build kurmak — yani Windows, Linux ve macOS için ayrı binary’leri otomatik üretiyorsun.
.NET runtime kurulu olmak zorunda mı?
Hayır, gerek yok. Native AOT zaten self-contained binary üretiyor, yani hedef makinede.NET runtime olması gerekmiyor. Bence bu dağıtım açısından gerçekten büyük bir avantaj.
Binary boyutu çok şişmiyor mu?
Açıkçası, C++ addon’larla kıyaslanırsa evet, biraz daha büyük oluyor. Basit bir addon için mesela 5-15 MB arası bir şey bekleyebilirsin. Trimming ayarlarıyla boyutu biraz aşağı çekebilirsin ama C++ seviyesine inmesi hani pek mümkün değil.
Bu hangi.NET versiyonundan itibaren kullanılabiliyor?
.NET 7’den itibaren Native AOT var ama tecrübeme göre asıl olgunluğa.NET 8 ile ulaştı. Yeni bir şey başlatıyorsan.NET 9 veya 10 hedeflemeni öneririm — mesela UnmanagedCallersOnly, LibraryImport gibi özellikler çok daha stabil artık.
N-API binding’leri için hazır bir NuGet paketi var mı?
Şu an ne resmî ne de yaygınlaşmış bir paket var maalesef. N-API fonksiyon imzalarını C#’ta kendin tanımlamak zorunda kalıyorsun. Topluluk tarafında bu yönde çalışmalar yürüyor ama henüz olgunlaşmadı — bence zamanla bu boşluk dolacak.
Kaynaklar ve İleri Okuma
Writing Node.js addons with.NET Native AOT —.NET Blog
Native AOT deployment — Microsoft Learn
Tuhaf ama, Node-API (N-API) — Node.js Official Documentation
Bu içerik işinize yaradı mı?
Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.








3 comments