Şimdi yükleniyor

Node.js Addon’larını .NET Native AOT ile Yazmak

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.

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.

node-gyp Derdi: Neden Değişiklik Gerekiyor?

Node.js tarafında native addon yazmaya kalkınca, iş dönüp dolaşıp node-gyp’e geliyor. C ya da C++ ile bir shared library yazıyorsunuz, sonra node-gyp önü derlemeye çalışıyor ve tam o anda küçük görünen ama 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ı GitHub Actions üzerindeydi ve her build’de Python kurulumu ile node-gyp derlemesi toplamda yaklaşık üç dakika yutuyordu. Üç dakika az gibi dürüyor, 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ı de 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. 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, yanı 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.

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. 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, yanı 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 (tabi 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 anlatsamdı…

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 dürüyor, evet. PublishAot ile “bana shared library üret” diyorsunuz; AllowUnsafeBlocks işe 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, 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; yanı 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 da modül sanki yerleşikmiş gibi davranıyor. Peki neden önemli? Çünkü işim tarafında ufak bir kayma bile olursa, sonra dönüp saç baş yoluyorsunuz.

RegisterFunction Detayları

Aslında, Fonksiyon kaydı kısmı aslında 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: 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 ve kurumsal dashboard uygulamalarında bu iş dönüp dolaşıp Windows servislerine, Registry’ye ya da platform-specific API’lere dayanıyor.

Bir finans kuruluşunda geçen yıl birebir buna benzer bir şey 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ş, build patlamış, ekip de ne olduğunu anlamaya çalışıyor. Ekip 6 kişiydi, 2 kişi.NET biliyordu, C++ bilen işe yoktu; peki addon’da bir bug çıksa kim düzeltecek?

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 ortaminde rahat ediyorsanız ve 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 — bence 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, C++ toolchain, node-gyp Sadece.NET SDK
CI/CD süresi Derleme süresi uzun Publish AOT süresi var ama tek seferlik
Yeni geliştirici onboarding Zor — birçok araç kurulumu Kolay —.NET SDK yeterli
Debug deneyimi C++ debugging (zor) C# debugging (daha kolay)
Binary boyutu Küçük Daha büyük (~5-15 MB)

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, node-gyp derken kurulum zinciri uzuyor. Ekipte yeni biri varsa ilk gün biraz sürünüyor.

Şöyle ki, C# Native AOT tarafında işe tablo tersine dönüyor. Build daha temiz (belki yanılıyorum ama) hissettiriyor, debug tarafı da daha rahat ilerliyor, ama çıkış dosyası büyüyor; yanı “şey, bu niye bu kadar şişti?” diye bakınca cevap net: runtime’ı gömüyorsun. 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 yazımızda bu konuya da değinmiştik.

Binary boyutu konusunda açık konuşayım: Native AOT çıktısı C++ ile karşılaştırıldığında büyük. 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 bir 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 yazımızda bu konuya da değinmiştik. Foundry Local GA Öldü: Bulut Olmadan Yerel AI yazımızda bu konuya da değinmiştik.

Kısa bir not düşeyim buraya. Daha fazla bilgi için SQL MCP Server: Veritabanını Ajanlara Açmanın Yolu yazımıza bakabilirsiniz.

Bakın, 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.

Pratik Adımlar: Nereden Başlamalı?

İlk Adım: Basit Bir “Hello World” Addon

Bak şimdi, bu işe devasa bir şeyle girişmeyin. Küçük başlayın. Önce basit bir addon yazın; JavaScript’ten bir fonksiyon çağırın, sadece string dönsün. Çalıştığını görünce gerisi zaten daha rahat geliyor, yoksa ilk günden her şeyi aynı anda çözmeye çalışınca insan biraz dağılıyor.

  1. .NET 9 veya 10 SDK’yı yükleyin (net10.0 hedefliyorsanız preview gerekebilir)
  2. Yukarıdaki minimal proje dosyasını oluşturun
  3. Entry point fonksiyonunu yazın
  4. dotnet publish -r win-x64 -c Release ile yayımlayın
  5. Çıkan.dll’i Node.js’te require() ile yükleyin

Bir dakika, şunu da ekleyeyim: N-API type’larını C#’ta tanımlamak biraz zahmetli oluyor. napi_value, napi_env gibi opaque pointer’lar, napi_create_string_utf8 gibi fonksiyon imzaları… Bunları tek tek elle yazmak yerine bir helper sınıfı kurmak daha mantıklı geliyor. İşte, c# Dev Kit ekibi de bunu yapmış zaten; tüm N-API binding’lerini tek bir partial class içinde toplamışlar, iş biraz orada toparlanıyor.

💡 Bilgi: Native AOT ile publish ettiğinizde çıkan dosya self-contained olur — yanı hedef makinede.NET runtime kurulu olmasına gerek yok. Bu, dağıtım tarafında baya iş görüyor.

Cross-Platform Derleme Stratejisi

Dürüst olmak gerekirse, Peki neden burada durup düşünmek lazım? Çünkü Native AOT cross-compilation desteklemiyor, en azından şu an için durum bu (inanın bana). Yanı Windows’ta oturup Linux binary’si üretmeye kalkarsanız olmuyor. Her platform için o platformda publish yapmanız gerekiyor; biraz uğraştırıyor ama pipeline tarafında matrix build ile çözülebiliyor (GitHub Actions’ta runs-on: [windows-latest, ubuntu-latest, macos-latest] gibi).

Bunu daha önce Azure DevOps Server Nisan Yaması: Ne Geldi, Ne Yapmalı? yazımda Azure DevOps bağlamında ele almıştım. Pipeline stratejisi kurumsal projelerde kritik oluyor, hani bazen asıl farkı kod değil o düzen belirliyor.

Şöyle ki, Neyse, çok dağıtmadan söyleyeyim: doğru derleme modeliyle ilerlerseniz hem paketleme kolaylaşıyor hem de dağıtımda sürpriz azalıyor. Sız ne dersiniz?

Az Önce Atladığım Bir Şey: Hata Yönetimi

Açık konuşayım, N-API ile uğraşırken hata işi biraz can sıkıyor. C seviyesinde bir API ile boğuşuyorsunuz, exception yok, sihir yok, her çağrıdan sonra status bakmanız gerekiyor; ben de C# tarafında bunu daha katlanılır hâle getirmek için küçük bir yardımcı yazıyorum:

static void CheckStatus(napi_status status)
{
if (status != napi_status.napi_ok)
throw new InvalidOperationException(
$"N-API call failed with status: {status}");
}

İyi hoş da, dur bir saniye — [UnmanagedCallersOnly] ile işaretlenmiş fonksiyonlardan exception fırlatmak doğrudan olmuyor. Orada yakalayıp N-API’nın kendi hata mekanizmasına (napi_throw_error) çevirmek lazım; ilk denememde bunu kaçırmıştım ve addon pat diye crash etti, Node.js process’i de beraberinde gitti. Evet, pek keyifli değildi.

Şimdi gelelim işin can alıcı noktasına.

.NET tarafındaki yeni şeyleri takip ediyorsanız, .NET 11 Preview 3: Gelen Yenilikler ve Sahadan Notlar yazıma da bakın derim — Native AOT tarafında da ufak ama iş gören iyileştirmeler geliyor, yanı boş güncelleme değil gibi dürüyor.

Enterprise vs Küçük Ekip: Kim İçin Mantıklı?

Büyük bir kurumsal yapıda çalışıyorsanız, hele bir de.NET tarafına zaten yakınsanız, bu geçiş baya mantıklı geliyor. Peki neden? Çünkü bazı yerlerde insan “ya bunu niye hâlâ Python bağımlılığıyla taşıyoruz” diye düşünüyor; özellikle CI pipeline’larında, güvenlik tarafında. Bakım yükünde iş bir anda daha sade hâle geliyor.

  • CI pipeline’larınızda Python bağımlılığından kurtulmak istiyorsanız (bu kritik)
  • Ekibiniz C++ bilmiyor ama C# biliyor (bu kritik)
  • Platform-specific API’lere erişmeniz gerekiyorsa (Registry, WMI, Certificate Store)
  • Güvenlik ekibiniz bağımlılık sayısını azaltmanızı istiyorsa (bence en önemlisi)

Bence, Küçük ekipseniz ya da startup’taysanız durum biraz değişiyor, hatta açık konuşayım bazen tam tersine dönebiliyor. Eğer elinizde sadece bir-iki native fonksiyon varsa ve ekipte.NET bilen yoksa, belki Rust + neon veya doğrudan WASM gibi seçenekler daha pratik kalır; yanı mesele teknoloji değil, bağlam. Mantıklı değil mi? Gümüş kurşun yok, o kadar basit.

Ha bir de maliyet kısmı var. Azure DevOps veya GitHub Actions’ta CI build süresi bildiğin para demek; node-gyp derleme adımını çıkarınca pipeline başına 2-4 dakika kazanabiliyorsunuz, bu da ayda 500 build yapan bir ekipte küçümsenecek gibi durmuyor. Yanı tek tek bakınca ufak görünen şeyler, ay sonunda garip şekilde büyüyor. Bulut Maliyet Optimizasyonu: Hâlâ Geçerli Prensipler yazımda da bu tarz “küçük ama birikimli” tasarruflara değinmiştim.

Evet, doğru duydunuz.

Kağıt Üstü Süper, Pratikte Ne Kadar Olgun?

Garip gelecek ama, Bu konuda %100 emin değilim ama açık konuşayım: yaklaşım daha yeni sayılır. Daha açık söyleyeyim, c# Dev Kit ekibinin bunu production’da kullanıyor olması insana bir nebze güven veriyor, fakat topluluk tarafında iş henüz çok yaygın değil; N-API binding’leri için hazır bir NuGet paketi de ben en azından göremedim, yanı çoğu şeyi elle kurcalıyorsunuz (en azından benim deneyimim böyle). Biraz ham dürüyor, işin aslı bu (yanlış duymadınız)

Güzel tarafı var, evet. Ama dur bir saniye — biraz daha pişmesi lazım gibi geliyor bana. Eğer ortada bir NuGet paketi çıksa, mesela NodeApi.Interop gibi bir şey olsa. N-API type tanımlarıyla helper’lar yanına hazır gelse, benimseme hızı baya değişir; belki biri open-source olarak yapar, belki Microsoft kendi çıkarır. Göreceğiz artık.

Benim tavsiyem şu: şimdilik deneysel işlerde kurcalayın, production’a almadan önce binary boyutuna bakın, startup süresini ölçün. Hata yönetimini didik didik test edin (çünkü küçük görünen şeyler sonra can sıkabiliyor). Ama radarınızda kalsın.

Sıkça Sorulan Sorular

Native AOT ile derlenen addon her platformda çalışıyor mu?

Hayır, maalesef çalışmıyor. Peki, 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 — yanı 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, yanı 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?

Araya gireyim: Açıkçası, C++ addon’larla kıyaslanırsa evet, biraz daha büyük oluyor. Basit bir addon için 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

İtiraf edeyim, Node-API (N-API) — Node.js Official Documentation

İçeriği paylaş:

Aşkın KILIÇ

20+ yıl deneyimli Azure Solutions Architect. Microsoft sertifikalı bulut mimari ve DevOps danışmanı. Azure, yapay zekâ ve bulut teknolojileri üzerine Türkçe teknik içerikler üretiyor.

AZ-305AZ-104AZ-500AZ-400DP-203AI-102

Bu içerik işinize yaradı mı?

Benzer içerikleri kaçırmamak için beni sosyal medyada takip edin.

Haftalık Bülten

Her pazar özenle seçilmiş teknoloji yazıları doğrudan e-postanıza gelsin.

0 comments

comments user
Serkan D.

node-gyp kurulumu her seferinde ayrı bir acı, özellikle Windows’ta. .NET tarafında bu işleri halledebilmek gerçekten ilginç bir alternatif, AOT ile boyut ve startup süresi nasıl çıkıyor merak ettim?

comments user
Murat Ö.

node-gyp ile uğraşmak gerçekten baş belası, Python versiyonu tutmayan, MSVC bulamayan derlemeler derken saatler harcadım. .NET Native AOT yaklaşımı ilginç görünüyor ama boyut ve startup süresi nasıl, karşılaştırmalı bir benchmark görmek isterdim. Bu arada Kubernetes tarafında da güzel bir yazınız varmış: https://www.askinkilic.com.tr/kuberneteste-ai-agent-sandbox-pratik-rehber/

comments user
Arda K.

node-gyp kurulumu derken saatler harcadığımı hatırladım, hele CI tarafında her seferinde Python versiyonu derdi çıkardı. .NET AOT ile bu acıdan kurtulmak gerçekten cazip görünüyor, peki performans farkı belirgin mi?

Yorum gönder

Microsoft Azure Çözüm Uzmanı | Bulut Bilişim, Yapay Zekâ, DevOps ve Kurumsal Güvenlik alanlarında 15+ yıl deneyim. Azure, Kubernetes, AI/ML ve modern altyapı mimarileri üzerine yazılar yazıyorum.

SİZİN İÇİN DERLEDİK

Haftalık Bülten

Azure, DevOps ve Yapay Zeka dünyasındaki en güncel içerikleri her hafta doğrudan e-postanıza alın.

Spam yok. İstediğiniz zaman iptal edebilirsiniz.
📱
Uygulamayı Yükle Ana ekrana ekle, çevrimdışı oku
Paylaş
İçindekiler
    ← AI Maliyet Optimizasyonu: ROI&...
    Kubernetes’te AI Agent S... →
    📩

    Gitmeden önce!

    Her pazar özenle seçilmiş teknoloji yazıları ve AI haberleri doğrudan e-postanıza gelsin. Ücretsiz, spam yok.

    🔒 Bilgileriniz güvende. İstediğiniz zaman ayrılabilirsiniz.

    📬 Haftalık bülten: Teknoloji + AI haberleri