Ketika kita menulis kode C#, kode tidak langsung di-compile menjadi machine instruction untuk prosesor atau operating system spesifik. Kode C# tersebut harus melewati beberapa tahap , disebut compilation pipeline, yang didesain untuk mendukung tiga hal: portabilitas (bisa jalan di banyak OS), keamanan, dan optimisasi.
Sebelum masuk ke detail, ini gambaran besarnya:
- Source Code (.cs)
- ⇒ Rosyln (Compiler) – Cek syntax, type safety, language rules
- Assembly (.dll/.exe), berisi: IL + metadata + manifest + resources + (PDB terpisah)
- CLR (.NET Runtime) .NET Runtime: memuat assembly, JIT-compile IL, garbage collection, exception handling, dll
- Native Machine Code (spesifik OS + arsitektur prosesor)
- Program benar-benar berjalan
1. Roslyn, C# Compiler
Langkah pertama dalam pipeline adalah C# compiler, yang dikenal sebagai Roslyn. Roslyn menganalisis source code untuk ketepatan, mengecek syntax, type safety, dan language rules sebelum menghasilkan compiled output.
Roslyn juga memberdayai code analysis features pada tools seperti Visual Studio dan VS Code, termasuk IntelliSense, code navigation, dan real-time error detection. Selain itu, Roslyn memberdayai static code analysis tools yang mengecek kualitas kode, style issues, dan potensi masalah keamanan.
Jika kodenya valid, compiler (Roslyn) menerjemahkannya ke Intermediate Language (IL).
Contoh: kode C# sederhana ini…
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
…akan gagal di-compile oleh Roslyn kalau ada type mismatch, misalnya:
public int Add(int a, int b)
{
return a + "b"; // Compile-time error: CS0029
// Cannot implicitly convert type 'string' to 'int'
}
Error ini ditangkap sebelum kode sempat jadi IL sama sekali, ini bagian dari “ketepatan” yang dicek Roslyn.
Roslyn juga dapat dipakai secara programatik (Roslyn API) untuk analisis kode, contohnya untuk menghitung jumlah method dalam sebuah class:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
var code = "public class Foo { void A() {} void B() {} }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();
var methodCount = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Count();
Console.WriteLine($"Jumlah method: {methodCount}"); // 2
Ini contoh konkret dari “code analysis features” yang disebut di atas — analyzer di VS Code/Visual Studio pada dasarnya melakukan hal serupa di background.
Apa saja yang sebenarnya dihasilkan Roslyn?
Penting untuk dipahami: Roslyn tidak cuma menghasilkan “kode IL”. Outputnya adalah satu paket lengkap bernama assembly (.dll/.exe), yang di dalamnya berisi beberapa komponen berbeda:
- IL Code, instruksi bytecode untuk tiap method.
- Metadata, tabel data yang mendeskripsikan struktur program: semua class, method, field, property beserta signature-nya. Metadata inilah yang membuat reflection (
typeof(),GetType()) bisa bekerja tanpa perlu source code asli. - Assembly Manifest, deskripsi assembly itu sendiri: nama, versi, dan daftar referensi ke assembly lain yang dipakai.
- Resources (opsional), file embedded seperti string resource (
.resx) atau gambar. - Attributes, metadata dari attribute yang ditulis di kode, seperti
[Serializable]atau[Obsolete]. - Debug Symbols (
.pdb), file terpisah, dihasilkan saat build Debug, memetakan tiap instruksi IL kembali ke baris source code asli. File inilah yang membuat breakpoint dan stack trace bisa menunjuk ke baris kode yang tepat.
Calculator.dll
├── Assembly Manifest (nama: Calculator, versi 1.0.0.0, referensi: System.Runtime, ...)
├── Metadata (struktur: class Calculator, method Add(int, int): int)
├── IL Code (instruksi ldarg/add/ret untuk method Add)
└── Resources (kalau ada)
Calculator.pdb ← file terpisah, khusus debugging
Karena assembly bersifat self-describing (metadata-nya lengkap), CLR nanti bisa tahu seluruh struktur program hanya dari file ini, tidak perlu file header terpisah seperti di C/C++.
dotnet build = memanggil Roslyn (tapi bukan cuma itu)
dotnet build sebenarnya menjalankan proses MSBuild, dan Roslyn adalah salah satu langkah di dalamnya (langkah paling penting). Urutan kasarnya:
- dotnet build
- Restore dependencies → pastikan semua NuGet package tersedia
- MSBuild baca .csproj, tentukan file mana yang perlu di-compile
- Roslyn dipanggil → hasilkan IL + metadata + manifest → MyApp.dll
- Copy semua dependency .dll ke folder output
- Generate file pendukung → MyApp.deps.json, MyApp.runtimeconfig.json
- (kalau Debug) generate MyApp.pdb
Begitu dotnet build selesai, MyApp.dll yang dihasilkan sudah 100% output Roslyn , tidak ada compile tambahan lagi setelah ini. Perlu diketahui juga: dotnet run sebenarnya melakukan dotnet build dulu secara implisit (kecuali dipakai --no-build), baru menjalankan hasilnya.
2. Intermediate Language (IL)
IL adalah representasi low-level platform independent dari sebuah program. Dan karena IL tidak terikat pada spesifik OS atau arsitektur prossesor, aplikasi yang telah di-compiled bisa berjalan pada envrionment yang berbeda tanpa harus direcompiled dari source code.
Kalau Calculator.cs di atas di-compile dan IL-nya dilihat (misalnya lewat ildasm atau sharplab.io), method Add akan terlihat kira-kira seperti ini:
.method public hidebysig instance int32 Add (
int32 a,
int32 b
) cil managed
{
.maxstack 8
IL_0000: ldarg.1 // load a
IL_0001: ldarg.2 // load b
IL_0002: add // a + b
IL_0003: ret
}
Perhatikan: instruksi seperti ldarg dan add ini bukan instruksi native CPU — ini bytecode CIL (Common Intermediate Language) yang masih perlu diterjemahkan lagi ke native code oleh runtime. Inilah yang membuat file .dll/.exe hasil build C# bisa jalan di Windows, Linux, maupun macOS selama ada .NET runtime yang sesuai.
Poin penting: IL yang identik di Windows dan Linux
Untuk source code yang sama, Roslyn menghasilkan IL yang byte-identik, baik di-compile di Windows maupun Linux , karena Roslyn cuma menerjemahkan syntax C# ke instruksi IL yang platform-independent, tidak peduli OS tempat dia dijalankan.
Windows: dotnet build → Calculator.dll (IL) , bytes identik
Linux: dotnet build → Calculator.dll (IL) , bytes identik
Ini artinya Calculator.dll hasil compile di Windows bisa disalin ke Linux dan tetap bisa dijalankan (selama ada CLR di Linux) , dibahas lebih detail di bagian akhir catatan ini.
3. .NET Runtime (CLR), Eksekusi
CLR (Common Language Runtime) adalah runtime environment milik .NET yang bertanggung jawab mengeksekusi aplikasi setelah proses compile Roslyn selesai. Tugas utamanya:
- Memuat assembly , membaca file
.dll/.exe(IL + metadata). - JIT compiler , menerjemahkan IL ke native machine code saat method pertama kali dipanggil.
- Memory management , termasuk garbage collector (GC), sehingga tidak perlu manual
malloc/freeseperti di C/C++. - Type safety & security enforcement , memastikan operasi sesuai type yang dideklarasikan.
- Exception handling , mekanisme
try/catch/finallyyang konsisten lintas bahasa .NET. - Thread management , dasar dari
Task,async/await,lock, dll.
CLR bukan satu file , dan bukan dibuat oleh Roslyn
CLR sebenarnya kumpulan beberapa file (library/binary) yang bersama-sama membentuk runtime, bukan satu file tunggal:
Linux (.NET 8, contoh path):
/usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.x/
├── libcoreclr.so ← inti CLR (engine eksekusi)
├── libclrjit.so ← JIT compiler
├── System.Private.CoreLib.dll ← base class library
└── ... (puluhan file lain)
Windows (path serupa):
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.x\
├── coreclr.dll
├── clrjit.dll
├── System.Private.CoreLib.dll
└── ...
CLR dan Roslyn adalah dua proyek yang sepenuhnya terpisah, dibuat tim berbeda, bahasa implementasi berbeda:
| Roslyn | CLR | |
|---|---|---|
| Peran | Compiler (front-end) | Runtime engine (back-end) |
| Ditulis dalam | C# | C++ (sebagian kecil assembly) |
| Tugas | Source code → IL + metadata | Muat assembly, JIT ke native code, GC, dll |
| Kapan jalan | Sekali, saat build | Berulang, setiap kali app dieksekusi |
| Repo | dotnet/roslyn | dotnet/runtime |
Hubungan keduanya bukan “membentuk”, tapi input-output berurutan: Roslyn menghasilkan produk (assembly IL), CLR adalah mesin yang mengonsumsi produk itu saat runtime. Roslyn sudah selesai bekerja jauh sebelum CLR bahkan dimuat.
Analogi: IL adalah “resep masakan” , Roslyn adalah “penulis resep”. CLR adalah “dapur beserta kokinya” yang membaca resep itu dan memasaknya jadi makanan (native code). Dapur tidak dibangun oleh penulis resep; keduanya diproduksi terpisah, cuma “bertemu” lewat resep sebagai perantara.
4. JIT vs AOT vs ReadyToRun
Setelah assembly dimuat CLR, ada tiga cara IL diterjemahkan ke native code:
a. Just-In-Time (JIT) , paling umum, default
- Kompilasi terjadi saat runtime, di mesin end-user, bukan di mesin developer.
- Method di-compile ke native code ketika pertama kali dieksekusi, bukan semuanya di awal.
- Compiled code disimpan di memory dan dipakai ulang selama lifetime aplikasi.
- Ada tiered compilation: JIT awalnya hasilkan kode “cepat tapi kurang optimal”, lalu method yang sering dipanggil (hot path) di-compile ulang dengan optimisasi lebih agresif.
Konsekuensinya: ada sedikit startup delay (warm-up) , makanya request pertama ke endpoint ASP.NET Core biasanya lebih lambat dibanding request berikutnya.
JIT bukan terikat pada dotnet run saja. JIT adalah perilaku level CLR, dan tetap terjadi di skenario mana pun selama IL dieksekusi tanpa precompilation:
# Semua skenario ini tetap memakai JIT di baliknya:
dotnet run
dotnet bin/Release/net8.0/MyApp.dll # hasil publish framework-dependent
dotnet test # saat test runner mengeksekusi kode
# + aplikasi ASP.NET Core yang di-hosting lewat IIS, Kestrel, atau container
Catatan penting soal file: JIT tidak pernah membuat file .dll baru. JIT terjadi setelah .dll (IL) dibaca, hasilnya (native machine code) cuma disimpan di memory, tidak pernah ditulis balik jadi file. Jadi tidak ada istilah “.dll hasil JIT” , yang ada cuma “.dll berisi IL” (dibuat Roslyn) yang nanti di-JIT saat dijalankan.
b. Ahead-Of-Time (AOT)
- IL dikompilasi ke native machine code sebelum aplikasi berjalan , saat proses publish, bukan saat runtime.
- Mengurangi startup time (tidak ada JIT saat app pertama jalan) dan meningkatkan predictability (tidak ada variasi performa dari warm-up).
- Cocok untuk container/serverless (cold start harus cepat), CLI tools, device resource terbatas.
- Trade-off: binary lebih besar, dan fitur reflection dinamis terbatas.
# Publish sebagai Native AOT (tersedia mulai .NET 7+)
# Linux
dotnet publish -c Release -r linux-x64 -p:PublishAot=true
# Windows
dotnet publish -c Release -r win-x64 -p:PublishAot=true
Catatan: proses ini tidak cross-compile secara default , publish
win-x64idealnya dilakukan di mesin Windows, publishlinux-x64di mesin Linux, karena Native AOT butuh native toolchain platform tersebut (Windows: Visual Studio Build Tools/C++ toolset; Linux:clang).c. ReadyToRun (R2R) , hybrid
Assembly di-precompile sebagian ke native code saat publish (mempercepat startup), tapi JIT tetap jalan di background untuk optimisasi lanjutan. Beda dari Native AOT yang menghilangkan JIT sepenuhnya.
# Linux dotnet publish -c Release -r linux-x64 --self-contained -p:PublishReadyToRun=true # Windows dotnet publish -c Release -r win-x64 --self-contained -p:PublishReadyToRun=true
5. Self-Contained vs Framework-Dependent
Ini bukan soal build vs publish , yang menentukan adalah flag --self-contained, dan ini bisa dipakai di kedua konteks (meski biasanya dipakai saat publish untuk deployment).
# Framework-dependent (default, TANPA flag)
dotnet publish -c Release
# → Hasilnya MASIH butuh .NET runtime terinstal di mesin tujuan
# Self-contained (WITH --self-contained -r <RID>)
dotnet publish -c Release --self-contained -r linux-x64 # Linux
dotnet publish -c Release --self-contained -r win-x64 # Windows
Kalau self-contained, seluruh runtime CLR (termasuk JIT compiler) ikut disalin ke folder output publish:
publish/linux-x64/ publish/win-x64/
├── MyApp ├── MyApp.exe
├── MyApp.dll ├── MyApp.dll
├── libcoreclr.so ←baru ├── coreclr.dll ←baru
├── libclrjit.so ←baru ├── clrjit.dll ←baru
└── ... └── ...
Cara menjalankan hasilnya:
# Linux , perlu prefix ./ karena current directory tidak ada di $PATH
./MyApp
# Windows (cmd/PowerShell) , bisa langsung panggil nama file
MyApp.exe
Ini bukan berarti JIT hilang , CLR yang dibundle tetap men-JIT-compile IL ke native code saat method pertama kali dieksekusi, cuma native code-nya dihasilkan spesifik untuk OS masing-masing. Self-contained menyelesaikan masalah distribusi (tidak perlu install runtime terpisah), bukan menghilangkan JIT.
Framework-dependent (build/run/publish tanpa flag) | Self-contained (--self-contained) | |
|---|---|---|
| CLR ikut dibundle di folder output? | Tidak | Ya |
Butuh .NET terinstal di mesin tujuan? | Ya | Tidak |
| Ukuran output | Kecil | Besar (puluhan MB) |
6. Apphost, apa itu MyApp.exe / MyApp?
File MyApp.exe (Windows) atau MyApp (Linux) yang muncul di folder bin//publish/ bukan CLR, dia disebut apphost: executable native kecil yang fungsinya jadi “pintu masuk” untuk memanggil CLR, bukan CLR itu sendiri.
publish/
├── MyApp / MyApp.exe ← APPHOST (native, kecil, bukan IL, bukan CLR)
├── MyApp.dll ← IL aplikasi (hasil Roslyn)
├── MyApp.deps.json
├── MyApp.runtimeconfig.json
├── libcoreclr.so / coreclr.dll ← CLR (kalau self-contained)
└── ...
Apphost sendiri tidak menjalankan aplikasi , dia cuma bootstrapper yang: cari MyApp.dll di folder yang sama → cari CLR (di folder sendiri kalau self-contained, atau di instalasi sistem kalau framework-dependent) → serahkan eksekusi ke CLR.
Analogi: kalau CLR itu “dapur” dan IL itu “resep”, maka apphost itu seperti tombol power yang menyalakan dapur dan menyuruhnya mulai memasak , bukan dapurnya, bukan juga makanannya, cuma pemicu paling depan.
7. Dua Direktori yang Berbeda
Penting untuk tidak tertukar antara dua lokasi ini:
a. Direktori output project (bin/Debug/net8.0/)
Isinya cuma milik project ini saja, dihasilkan dotnet build:
Windows: C:\Users\<user>\ProjekmU\bin\Debug\net8.0\
Linux: ~/ProjekmU/bin/Debug/net8.0/
├── Calculator.dll
├── Calculator.pdb
├── Calculator.deps.json
├── Calculator.runtimeconfig.json
└── Calculator / Calculator.exe ← apphost
hostfxr, coreclr, clrjit tidak ada di sini (kecuali publish self-contained).
b. Direktori instalasi .NET (shared runtime)
Dipakai bersama oleh semua aplikasi .NET di mesin itu, terpisah dari project manapun:
Windows: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\8.0.x\
Linux: /usr/share/dotnet/shared/Microsoft.NETCore.App/8.0.x/
├── (lib)coreclr / coreclr.dll
├── (lib)clrjit / clrjit.dll
└── ...
Windows: C:\Program Files\dotnet\host\fxr\8.0.x\
Linux: /usr/share/dotnet/host/fxr/8.0.x/
└── (lib)hostfxr , komponen yang mencari & memuat versi CLR yang tepat
Saat dotnet run, apphost/muxer di folder project mencari keluar ke direktori instalasi ini untuk memuat CLR , CLR tidak datang ke folder project (kecuali self-contained, di mana CLR disalin masuk ke folder output).
8. Alur Lengkap: Dua Jalur Eksekusi
Ada dua “pintu masuk” berbeda untuk menjalankan aplikasi yang sama , hasil akhirnya sama, tapi mekanismenya beda:
Jalur 1 , dotnet run / dotnet MyApp.dll (lewat muxer)
dotnet sendiri (yang disebut muxer) bertindak sebagai generic host untuk .dll apa saja yang diberikan sebagai argumen , apphost di folder bin/ sama sekali tidak disentuh, dilewati sepenuhnya:
Windows:
dotnet run
→ dotnet (muxer) baca argumen, panggil hostfxr.dll secara internal
→ hostfxr.dll mencari CLR terinstal di C:\Program Files\dotnet\shared\...
→ coreclr.dll dimuat
→ coreclr.dll membaca Calculator.dll (IL)
→ clrjit.dll JIT-compile method Add() → native x64 Windows machine code
→ dieksekusi
Linux:
dotnet run
→ dotnet (muxer) baca argumen, panggil libhostfxr.so secara internal
→ libhostfxr.so mencari CLR terinstal di /usr/share/dotnet/shared/...
→ libcoreclr.so dimuat
→ libcoreclr.so membaca Calculator.dll (IL) , file yang SAMA persis
→ libclrjit.so JIT-compile method Add() → native x64 Linux machine code
→ dieksekusi
Jalur 2 , ./MyApp / MyApp.exe (lewat apphost)
./MyApp dieksekusi (apphost, native code kecil, hostfxr ter-embed di dalamnya)
→ apphost cari CLR (folder sendiri kalau self-contained, sistem kalau framework-dependent)
→ CLR dimuat, baca MyApp.dll (dicari otomatis di folder yang sama), JIT, eksekusi
Perbandingan kedua jalur
dotnet run (muxer) | ./MyApp (apphost) | |
|---|---|---|
| Yang dieksekusi pertama | dotnet (terinstal sistem) | MyApp (di folder project/publish) |
Perlu tahu path .dll | Ya | Tidak, otomatis cari di folder sendiri |
| Apphost terpakai? | Tidak | Ya |
| Hasil akhir | Sama (CLR dimuat → JIT → eksekusi) | Sama |
Ini menjelaskan kenapa dotnet MyApp.dll tetap bisa jalan meski file apphost (MyApp.exe/MyApp) dihapus , jalur dotnet tidak pernah membutuhkan apphost sama sekali.
9. Apakah .dll Hasil Compile di Windows Bisa Dibaca di Linux?
Ya, bisa , dengan syarat yang dipindah adalah file .dll berisi IL (hasil Roslyn), bukan native code:
bash
# Di Windows
dotnet build
# → Calculator.dll (IL, belum di-JIT)
# Salin Calculator.dll ke mesin Linux (scp, USB, apa saja)
# Di Linux
dotnet Calculator.dll
# → BISA jalan, karena CLR di Linux membaca IL yang sama,
# lalu JIT-compile sendiri ke native code Linux
Yang tidak bisa dipindah lintas OS:
| Yang dipindah | Bisa jalan di Linux? |
|---|---|
Calculator.dll (hasil dotnet build, berisi IL) | Ya , CLR Linux baca IL, JIT sendiri secara lokal |
| Native AOT output dari Windows | Tidak , sudah native code Windows murni (Exec format error di Linux) |
Apphost MyApp.exe dari self-contained Windows | Tidak (tapi MyApp.dll di sebelahnya tetap bisa dipakai lewat dotnet MyApp.dll) |
Jadi: yang portable itu file IL sebelum di-JIT, bukan hasil dari proses JIT , karena hasil JIT memang sengaja spesifik-platform dan cuma hidup di memory selama proses berjalan, tidak pernah jadi file.
10. Ringkasan Besar
| Tahap | Apa yang terjadi | Kapan | Platform-dependent? |
|---|---|---|---|
| Roslyn compile | Source code → assembly (IL + metadata + manifest) | Development time (dotnet build) | Tidak , IL identik di semua OS |
| CLR load & JIT | Assembly dibaca, IL diterjemahkan ke native code | Runtime, saat app dijalankan | Ya , CLR & hasil JIT spesifik per OS |
| AOT compile (opsional) | IL → native code langsung, tanpa CLR/JIT saat runtime | Publish time | Ya , hasilnya spesifik OS, tidak cross-compile |
| Aspek | JIT | AOT (Native) |
|---|---|---|
| Kapan compile ke native code | Saat runtime, per-method, saat pertama dipanggil | Saat publish, sebelum aplikasi jalan |
| Startup time | Lebih lambat (ada warm-up) | Lebih cepat |
| Performa puncak (steady-state) | Bisa lebih optimal (tiered compilation) | Konsisten, tanpa optimisasi berbasis profil runtime |
| Ukuran output | Lebih kecil | Lebih besar |
| Reflection dinamis | Didukung penuh | Terbatas |
| Butuh .NET runtime di mesin target | Ya (kecuali self-contained) | Tidak |
Poin paling penting untuk diingat sebagai pemula:
- Roslyn = compiler, jalan sekali saat build, hasilkan assembly (IL + metadata) yang portable.
- CLR = runtime, kumpulan banyak file (bukan satu file, bukan dibuat Roslyn), jalan setiap kali app dieksekusi, hasilnya (native code lewat JIT) spesifik per OS dan cuma hidup di memory.
- Apphost (
MyApp.exe/MyApp) cuma “tombol power” pemanggil CLR , bukan CLR itu sendiri. dotnet runlewat jalur muxer (dotnet),./MyApplewat jalur apphost , dua pintu berbeda, hasil akhir sama.- File
.dllberisi IL itu portable lintas OS; yang tidak portable adalah hasil AOT atau apphost (karena itu sudah native code/executable OS-spesifik).

Leave a Reply