.NET di Balik Layar
24 Agustus 2026
•
2 jam baca
•
2 pembaca

Memahami bagaimana aplikasi .NET berjalan di Azure, mulai dari VM, container, hingga managed service.
Bagian 1: Komputer Tidak Memahami C#
Mulai dari sesuatu yang sangat sederhana. Baris kode berikut mungkin sudah ditulis ratusan kali:
C#
Console.WriteLine("Hello World");
Bagi manusia, baris tersebut terlihat sangat jelas. Console menunjukkan bahwa operasi berkaitan dengan terminal, WriteLine terdengar seperti perintah untuk menulis sebuah baris, dan "Hello World" adalah teks yang ingin ditampilkan. Bahkan tanpa memahami seluruh aturan C#, nama-nama tersebut masih cukup mudah ditebak maknanya.
Namun, pemahaman tersebut hanya berlaku pada tingkat bahasa pemrograman. CPU tidak memiliki pemahaman terhadap bahasa C#. CPU tidak mengetahui apa itu Console, tidak memahami arti WriteLine, tidak mengenal konsep class, object, string, async, await, LINQ, dan berbagai fitur lain yang tersedia dalam C#. CPU bekerja pada tingkat yang jauh lebih rendah.
CPU Hanya Memahami Instruksi Mesin
CPU pada dasarnya bekerja dengan menjalankan instruksi yang direpresentasikan dalam bentuk machine code. Instruksi tersebut merupakan pola bit yang dapat diproses oleh CPU sesuai dengan instruction set architecture (ISA) yang dimilikinya, misalnya x86-64 atau ARM64.
Pada tingkat ini, konsep seperti:
C#
Console.WriteLine("Hello World");
sudah tidak memiliki arti secara langsung.
CPU tidak melihat:
Plain Text
ConsoleWriteLine"Hello World"
sebagai tiga bagian yang bermakna. CPU hanya menerima instruksi yang menjelaskan operasi sangat dasar, misalnya memuat data, menyimpan data, melakukan operasi aritmatika, membandingkan nilai, atau melakukan lompatan ke instruksi lain.
Secara sederhana, gambaran tingkatannya dapat dibayangkan seperti ini:
Plain Text
C#││ Console.WriteLine("Hello World");│▼Bahasa tingkat tinggi││ diterjemahkan oleh compiler▼Instruksi tingkat rendah││ diterjemahkan ke instruksi yang sesuai dengan CPU▼Machine Code│▼CPU
Semakin ke bawah, bentuk program semakin jauh dari bahasa yang biasa dipahami manusia dan semakin dekat dengan cara kerja perangkat keras.
Lalu Apa yang Terjadi pada Console.WriteLine?
Ketika kode C# ditulis, sebenarnya belum ada instruksi CPU yang langsung dapat menjalankan baris tersebut. Kode tersebut masih merupakan source code, yaitu teks yang ditulis menggunakan aturan dan sintaks bahasa C#.
Sebagai contoh:
C#
Console.WriteLine("Hello World");
memiliki arti bagi compiler C#. Compiler memahami bahwa:
-
Consolemerupakan sebuah type yang tersedia dalam library .NET. -
WriteLinemerupakan sebuah method. -
"Hello World"merupakan sebuah string literal. -
;menandakan akhir statement. -
Keseluruhan statement meminta program memanggil method tersebut dengan sebuah nilai string.
CPU tidak memahami konsep-konsep tersebut. Compiler-lah yang memahami bahasa C# dan mengubah kode tersebut menjadi bentuk yang dapat diproses oleh lingkungan runtime .NET. Di sinilah compiler menjadi bagian penting dalam perjalanan sebuah program.
Compiler Sebagai Penerjemah
Compiler dapat dibayangkan sebagai penerjemah antara bahasa yang digunakan programmer dan bentuk program yang dapat diproses oleh komputer.
Manusia menulis:
C#
Console.WriteLine("Hello World");
Compiler membaca kode tersebut, menganalisis struktur dan maknanya, memeriksa apakah kode tersebut valid menurut aturan C#, lalu menghasilkan output yang sesuai dengan target platform.
Pada .NET, hasil kompilasi C# bukan langsung berupa instruksi CPU native dalam bentuk akhir yang akan dijalankan CPU. Compiler C# biasanya menghasilkan Intermediate Language (IL) yang disimpan di dalam sebuah .NET assembly, misalnya file .dll atau .exe.
Gambaran sederhananya:
Plain Text
Source Code C#││ Compiler C#▼Intermediate Language (IL)││ .NET Runtime▼Machine Code│▼CPU
Ini merupakan salah satu hal penting yang membedakan model eksekusi .NET dari gambaran sederhana "compiler langsung mengubah source code menjadi machine code".
Apa Itu Intermediate Language?
Intermediate Language, atau IL, adalah bentuk instruksi yang berada di antara source code dan machine code. IL tidak bergantung secara langsung pada jenis CPU tertentu seperti x86-64 atau ARM64. Karena itu, IL dapat menjadi bentuk perantara yang memungkinkan ekosistem .NET mendukung berbagai platform.
Sebagai contoh, sebuah program C# dapat dikompilasi menjadi assembly yang berisi IL. Assembly tersebut kemudian dapat dijalankan pada lingkungan .NET di berbagai sistem operasi dan arsitektur CPU selama runtime .NET yang sesuai tersedia.
Secara sederhana:
Plain Text
C# Source Code│▼IL│├──────────────► x64│├──────────────► ARM64│└──────────────► platform lain
Hal ini tidak berarti satu file IL dapat dijalankan tanpa adanya runtime. Program tetap membutuhkan komponen .NET yang bertugas memuat assembly, menyediakan library, mengelola memory, dan pada saat yang tepat mengubah IL menjadi instruksi native yang dapat dijalankan CPU.
Jadi, Apa yang Sebenarnya Menjalankan Program C#?
Pertanyaan ini membawa pembahasan menuju komponen penting dalam .NET: Common Language Runtime (CLR). CLR merupakan bagian dari .NET runtime yang menyediakan lingkungan untuk menjalankan managed code. Salah satu tugas pentingnya adalah menangani eksekusi IL.
Ketika sebuah method perlu dijalankan, runtime dapat menggunakan Just-In-Time compiler (JIT) untuk mengubah IL menjadi machine code yang sesuai dengan CPU yang sedang digunakan.
Gambaran yang lebih lengkap menjadi:
Plain Text
C# Source Code││ C# Compiler▼.NET Assembly││ berisi IL▼.NET Runtime / CLR││ JIT Compilation▼Native Machine Code│▼CPU
Dengan demikian, CPU memang tidak pernah benar-benar "mengerti C#". CPU hanya menjalankan machine code yang sesuai dengan instruction set yang dimilikinya.
Mengapa Tidak Langsung Menghasilkan Machine Code?
Pertanyaan berikutnya adalah: jika pada akhirnya CPU membutuhkan machine code, mengapa compiler C# tidak langsung menghasilkan machine code untuk CPU? Salah satu alasannya adalah fleksibilitas.
Bayangkan sebuah program dikompilasi secara langsung menjadi machine code khusus x86-64. Program tersebut akan sangat bergantung pada arsitektur tersebut. Jika program yang sama ingin dijalankan pada ARM64, dibutuhkan hasil kompilasi yang berbeda.
.NET menggunakan IL sebagai bentuk perantara. Source code dapat dikompilasi menjadi assembly yang berisi IL, kemudian runtime yang sesuai dengan platform dapat menangani eksekusinya.
Pendekatan ini membantu .NET mendukung berbagai kombinasi sistem operasi dan arsitektur CPU. Tentu saja, dalam praktik modern .NET terdapat beberapa model deployment dan compilation, termasuk ReadyToRun dan Native AOT. Jadi, tidak semua aplikasi .NET selalu mengikuti alur JIT secara identik. Namun, untuk memahami konsep dasar, alur C# → IL → JIT → machine code merupakan fondasi yang sangat penting.
C# Sebenarnya Hanya Salah Satu Bahasa di Atas .NET
Hal menarik lainnya adalah .NET tidak hanya memahami C#. Ekosistem .NET juga mendukung bahasa seperti Visual Basic dan F#. Bahasa-bahasa tersebut memiliki sintaks yang berbeda, tetapi dapat dikompilasi menuju ekosistem runtime .NET yang sama.
Secara sederhana:
Plain Text
C# ──────────┐│Visual Basic ├──► .NET Assembly / IL ──► CLR ──► CPU│F# ──────────┘
Hal ini memungkinkan berbagai bahasa menggunakan infrastruktur yang sama, seperti .NET runtime, garbage collector, type system, library, dan mekanisme eksekusi yang disediakan oleh platform .NET.
Jadi, CLR bukan "mesin yang hanya memahami C#". CLR bekerja pada level runtime .NET dan dapat menjalankan managed code yang mengikuti aturan dan format yang didukung oleh platform tersebut.
Dari Satu Baris Kode Menjadi Program yang Berjalan
Baris sederhana:
C#
Console.WriteLine("Hello World");
sebenarnya melewati beberapa tahapan sebelum menghasilkan teks di layar.
Secara konseptual:
Plain Text
1. Programmer menulis source code│▼2. Compiler C# membaca dan menganalisis source code│▼3. Compiler menghasilkan assembly yang berisi IL│▼4. .NET Runtime memuat assembly│▼5. CLR menangani eksekusi managed code│▼6. JIT mengubah IL menjadi native machine code│▼7. CPU menjalankan machine code│▼8. Program menghasilkan output
Setiap tahap memiliki tanggung jawab yang berbeda. Programmer berinteraksi dengan bahasa tingkat tinggi seperti C#. Compiler bertugas menerjemahkan dan memvalidasi kode. Assembly menyimpan hasil kompilasi. Runtime menyediakan lingkungan eksekusi. JIT menghasilkan native code ketika diperlukan. CPU akhirnya menjalankan instruksi tersebut.
C# Bukan Bahasa yang Dipahami CPU
Kesimpulan paling penting dari bagian ini adalah bahwa C# dan machine code berada pada dua tingkat yang sangat berbeda.
C# dibuat agar manusia dapat menulis program dengan cara yang lebih abstrak dan mudah dipahami. CPU bekerja dengan instruksi yang sangat spesifik terhadap perangkat keras.
Tidak ada konsep class, string, LINQ, atau async di dalam instruction set CPU. Konsep-konsep tersebut diterjemahkan dan ditangani oleh berbagai lapisan software sebelum pekerjaan akhirnya sampai kepada CPU.
Karena itu, ketika sebuah program C# berjalan, sebenarnya terdapat perjalanan panjang dari kode yang terlihat sederhana hingga instruksi yang benar-benar dieksekusi perangkat keras:
Plain Text
C# Source Code↓C# Compiler↓.NET Assembly↓IL↓CLR / .NET Runtime↓JIT↓Native Machine Code↓CPU
Perjalanan tersebut menjadi fondasi untuk memahami apa yang sebenarnya terjadi ketika sebuah aplikasi .NET dijalankan.
Bagian 2: Mengapa Source Code Perlu Di-compile?
Pada bagian sebelumnya, terlihat bahwa CPU tidak memahami C#. CPU hanya dapat menjalankan machine instructions yang sesuai dengan arsitektur CPU tersebut. Sementara itu, source code C# masih berupa teks yang ditulis menggunakan aturan bahasa pemrograman.
Muncul sebuah pertanyaan sederhana:
Bagaimana teks seperti Console.WriteLine("Hello World"); dapat berubah menjadi sesuatu yang akhirnya bisa dijalankan oleh CPU?
Jawabannya adalah compiler.
Apa Itu Compiler?
Compiler adalah sebuah program yang bertugas menerjemahkan source code dari bahasa pemrograman tingkat tinggi menjadi bentuk lain yang dapat diproses oleh komputer atau runtime.
Source code yang ditulis programmer masih menggunakan konsep-konsep yang nyaman bagi manusia, seperti variable, method, class, object, condition, loop, dan berbagai abstraksi lainnya.
Sebagai contoh:
C#
int age = 26;if (age >= 18){Console.WriteLine("Adult");}
Bagi manusia, kode tersebut cukup mudah dipahami. Namun CPU tidak mengetahui apa arti int, age, if, Console, atau WriteLine.
Compiler memahami aturan bahasa C# dan mengubah source code tersebut menjadi bentuk yang lebih dekat dengan cara kerja runtime.
Gambaran paling sederhananya:
Plain Text
C# Source Code↓Compiler↓Output
Namun untuk .NET, kata output tersebut perlu dijelaskan lebih lanjut.
Pada beberapa bahasa pemrograman, compiler memang dapat menghasilkan machine code sebagai hasil akhirnya. Misalnya, compiler C atau C++ dapat menghasilkan native executable yang berisi instruksi machine code untuk arsitektur tertentu.
C# menggunakan pendekatan yang berbeda.
Compiler C# umumnya menghasilkan Intermediate Language (IL), bukan machine code CPU sebagai hasil utama compilation.
Alurnya menjadi:
Plain Text
C# Source Code↓C# Compiler↓Intermediate Language (IL)↓.NET Runtime↓Native Machine Code↓CPU
Jadi, compilation belum berarti source code C# langsung berubah menjadi instruksi CPU.
Mengapa Source Code Harus Di-compile?
Compilation bukan sekadar proses "mengubah tulisan menjadi bentuk lain". Compiler juga melakukan berbagai pekerjaan penting sebelum program dapat dijalankan. Salah satunya adalah memeriksa apakah source code mengikuti aturan bahasa C#.
Misalnya terdapat kode:
C#
int age = "Hello";
Compiler dapat menemukan bahwa nilai "Hello" tidak sesuai dengan tipe int.
Kode tersebut secara sintaks mungkin terlihat seperti sebuah assignment yang valid, tetapi secara tipe tidak valid. Compiler dapat menghentikan proses compilation dan memberikan error.
Contoh lainnya:
C#
Console.WriteLnie("Hello");
Jika method yang dimaksud sebenarnya bernama WriteLine, compiler dapat mendeteksi bahwa WriteLnie tidak ditemukan.
Dengan demikian, compiler juga berfungsi sebagai salah satu lapisan pemeriksaan terhadap source code.
Secara sederhana, compilation dapat dibayangkan sebagai proses:
Plain Text
Source Code↓Parsing↓Pemeriksaan aturan dan tipe↓Analisis kode↓IL
Detail internal compiler jauh lebih kompleks, tetapi gambaran tersebut cukup untuk memahami konsep dasarnya.
Source Code Hanyalah Teks
Hal penting yang sering terlewat adalah bahwa file .cs pada dasarnya hanyalah file teks.
Misalnya terdapat file:
Plain Text
Program.cs
yang berisi:
C#
Console.WriteLine("Hello World");
File tersebut tidak bisa begitu saja diberikan kepada CPU.
CPU tidak mempunyai kemampuan untuk membuka Program.cs, membaca karakter C, o, n, s, o, l, dan kemudian memahami bahwa seluruhnya membentuk sebuah type bernama Console. File .cs adalah representasi program yang ditujukan untuk manusia dan compiler. Compiler kemudian memproses teks tersebut berdasarkan grammar dan aturan bahasa C#. Hasilnya bukan lagi sekadar teks C#.
Mengapa Tidak Langsung Menjadi Machine Code?
Sampai titik ini mungkin muncul pertanyaan:
Jika tujuan akhirnya adalah CPU, mengapa compiler C# tidak langsung menghasilkan machine code?
Secara teknis, .NET juga memiliki mekanisme yang memungkinkan compilation lebih dekat atau langsung menuju native code, seperti Native AOT. Namun model utama yang digunakan dalam banyak aplikasi .NET secara tradisional memanfaatkan IL sebagai intermediate representation.
Ada beberapa alasan penting di balik desain tersebut.
1. Portability
Machine code sangat bergantung pada arsitektur CPU. Machine code untuk x86-64 berbeda dengan machine code untuk ARM64. Misalnya sebuah aplikasi dikompilasi secara langsung menjadi native machine code x86-64. Hasil tersebut tidak dapat begitu saja dianggap sebagai native machine code ARM64.
Jika compilation selalu langsung menghasilkan machine code, compiler perlu menghasilkan output yang berbeda untuk berbagai target hardware. Dengan IL, source code dapat terlebih dahulu dikompilasi menjadi bentuk yang relatif independen terhadap CPU tertentu.
Plain Text
┌──► x86-64│C# → IL → Runtime│└──► ARM64
Runtime kemudian menangani tahap yang berkaitan dengan platform tempat aplikasi dijalankan. Inilah salah satu alasan mengapa IL penting dalam desain .NET.
2. Memisahkan Bahasa dari Hardware
Compiler C# terutama bertanggung jawab memahami bahasa C#.
Compiler perlu memahami hal-hal seperti:
C#
class Person{public string Name { get; set; }}
Compiler perlu mengetahui bahwa kode tersebut merupakan deklarasi class, property, tipe string, dan berbagai aturan C# lainnya. Compiler tidak harus menentukan seluruh detail instruksi CPU yang nantinya akan digunakan untuk menjalankan setiap method. Tanggung jawab tersebut dapat diserahkan kepada runtime dan komponen seperti JIT.
Pemisahan tanggung jawab ini dapat digambarkan sebagai:
Plain Text
C# Compiler││ memahami C#▼IL││ Runtime/JIT memahami platform▼Machine Code
Dengan desain seperti ini, bahasa pemrograman dan hardware tidak perlu terikat secara langsung pada tahap compilation yang sama.
3. Satu Runtime untuk Banyak Bahasa
IL juga memungkinkan berbagai bahasa pemrograman menggunakan infrastruktur .NET yang sama.
Sebagai contoh:
Plain Text
C# ───────────────┐│Visual Basic ─────┼──► IL ──► .NET Runtime ──► CPU│F# ───────────────┘
C#, Visual Basic, dan F# memiliki syntax yang berbeda.
Namun setelah compilation, program-program tersebut dapat menghasilkan managed code yang dapat dijalankan oleh .NET runtime. Inilah salah satu alasan hubungan antara bahasa pemrograman dan platform .NET perlu dibedakan.
C# adalah sebuah bahasa pemrograman, sedangkan .NET adalah sebuah platform yang menyediakan runtime, library, tooling, dan infrastruktur untuk menjalankan berbagai aplikasi.
Dengan kata lain:
Plain Text
C# ≠ .NET
C# adalah salah satu bahasa yang dapat digunakan untuk membangun aplikasi di atas .NET.
4. Optimasi Dapat Dilakukan Saat Runtime
Keuntungan lainnya berasal dari fakta bahwa machine code tidak harus ditentukan sepenuhnya saat source code pertama kali di-compile.
Ketika runtime menjalankan aplikasi, runtime mengetahui informasi mengenai environment yang sedang digunakan.
Misalnya:
Plain Text
Operating System : WindowsCPU : x86-64
atau:
Plain Text
Operating System : LinuxCPU : ARM64
JIT dapat menggunakan informasi tersebut ketika mengubah IL menjadi native machine code. JIT juga dapat melakukan optimasi berdasarkan kode yang sedang dijalankan.
Hal ini menghasilkan pemisahan menarik:
Plain Text
Compilation↓C# → IL↓Runtime↓IL → Native Code
Compilation dan execution tidak sepenuhnya menjadi satu proses.
Compiler menghasilkan representasi perantara, sedangkan runtime menangani tahap eksekusi lebih lanjut.
Apa Sebenarnya IL Itu?
IL merupakan singkatan dari Intermediate Language. Dalam ekosistem .NET, istilah lain seperti CIL (Common Intermediate Language) dan MSIL juga sering ditemukan dalam berbagai referensi.
IL dapat dipandang sebagai instruksi untuk virtual execution environment .NET, bukan instruksi langsung untuk CPU tertentu.
Contohnya, source code:
C#
int result = 10 + 20;
tidak harus langsung berubah menjadi instruksi x86-64 atau ARM64 pada saat compiler C# bekerja.
Compiler dapat menghasilkan IL yang merepresentasikan operasi tersebut.
Secara konseptual:
Plain Text
C#:int result = 10 + 20;↓ CompilerIL:instruksi .NET untukmemuat nilai,melakukan operasi,dan menyimpan hasil↓ Runtime / JITMachine Code:instruksi spesifik CPU↓CPU
IL menjadi jembatan antara bahasa pemrograman dan hardware.
IL Tidak Berarti "Machine Code Versi Sederhana"
IL bukan sekadar machine code yang ditulis menggunakan kata-kata. IL memiliki instruction set sendiri yang dirancang untuk lingkungan eksekusi .NET. Instruksi IL bekerja bersama metadata dan type system .NET. Karena itu, sebuah assembly .NET tidak hanya berisi instruksi. Assembly juga membawa informasi mengenai type, method, reference terhadap assembly lain, dan berbagai metadata lainnya. Hal ini memungkinkan runtime memahami struktur program ketika aplikasi dimuat.
Sebagai contoh, runtime perlu mengetahui bahwa terdapat sebuah type bernama:
Plain Text
Person
yang memiliki method:
Plain Text
GetName()
Informasi semacam ini merupakan bagian dari metadata yang terdapat dalam assembly. Jadi, hasil compilation .NET bukan sekadar "kode lain".
Lebih tepat jika dibayangkan sebagai:
Plain Text
.NET Assembly├── IL├── Metadata├── Type Information└── References
Detail struktur assembly akan dibahas lebih jauh pada bagian berikutnya.
Mengapa Tidak Mengompilasi C# Langsung ke Setiap CPU?
Tanpa intermediate representation, compiler perlu menangani lebih banyak kombinasi.
Bayangkan terdapat:
Plain Text
3 bahasa×3 sistem operasi×2 arsitektur CPU
Secara konseptual, semakin banyak kombinasi bahasa dan target hardware, semakin banyak variasi compilation yang harus dipertimbangkan.
Dengan adanya IL, modelnya menjadi lebih terpisah:
Plain Text
C# ───────────┐VB ───────────┼──► ILF# ───────────┘│▼.NET Runtime│┌───────┴───────┐▼ ▼x86-64 ARM64
Bahasa cukup menghasilkan bentuk perantara yang sesuai dengan ekosistem .NET. Runtime kemudian menangani perbedaan platform.
Tentu saja, implementasi nyata .NET jauh lebih kompleks daripada diagram tersebut. Namun secara arsitektur, pemisahan ini merupakan salah satu ide penting yang membuat platform .NET dapat berkembang ke banyak bahasa, sistem operasi, dan arsitektur hardware.
Apakah Compilation Berarti Program Sudah Bisa Berjalan?
Belum tentu. Compilation dan execution adalah dua hal yang berbeda.
Misalnya source code berhasil di-compile:
Plain Text
Program.cs↓Compiler↓Program.dll
Keberhasilan compilation berarti compiler berhasil menghasilkan output berdasarkan source code yang diberikan.
Namun ketika Program.dll dijalankan, masih ada proses lain yang terjadi:
Plain Text
Program.dll↓.NET Runtime↓Assembly Loading↓CLR↓JIT↓Native Code↓CPU
Pada tahap execution, runtime perlu memuat assembly, menemukan informasi yang diperlukan, menyiapkan environment, dan menangani berbagai kebutuhan aplikasi selama program berjalan.
Karena itu, berhasil di-compile tidak sama dengan sudah dieksekusi. Compilation menghasilkan sesuatu yang siap digunakan oleh tahap berikutnya. Execution adalah proses menjalankan hasil tersebut.
Gambaran Besar Sejauh Ini
Sampai titik ini, perjalanan sebuah aplikasi C# dapat digambarkan sebagai berikut:
Plain Text
┌──────────────────────┐│ C# Source Code ││ Program.cs │└──────────┬───────────┘││ C# Compiler▼┌──────────────────────┐│ .NET Assembly ││ ││ IL + Metadata │└──────────┬───────────┘││ .NET Runtime▼┌──────────────────────┐│ CLR ││ ││ Runtime Services │└──────────┬───────────┘││ JIT▼┌──────────────────────┐│ Native Machine Code │└──────────┬───────────┘│▼┌─────┐│ CPU │└─────┘
Setiap lapisan memiliki tanggung jawab yang berbeda.
C# source code adalah kode yang ditulis menggunakan bahasa yang nyaman bagi manusia. Compiler memahami bahasa C# dan menerjemahkannya menjadi bentuk yang sesuai untuk ekosistem .NET. Assembly menjadi wadah hasil compilation, termasuk IL dan metadata. CLR dan .NET runtime menyediakan lingkungan untuk menjalankan managed code. JIT dapat menerjemahkan IL menjadi native machine code. CPU akhirnya menjalankan native machine code tersebut.
IL Menjadi Titik Pertemuan
Dengan memahami IL, hubungan antara C# dan .NET mulai terlihat lebih jelas. C# bukan runtime. C# bukan CLR. C# juga bukan .NET secara keseluruhan.
C# adalah bahasa pemrograman yang dapat digunakan untuk menghasilkan managed code yang kemudian dijalankan oleh .NET.
.NET menyediakan platform yang lebih luas, termasuk runtime dan library yang memungkinkan aplikasi tersebut berjalan.
Hubungannya dapat disederhanakan menjadi:
Plain Text
Bahasa│┌───────┼───────┐│ │ │C# VB F#│ │ │└───────┼───────┘▼IL│▼.NET Runtime│▼CPU
IL menjadi salah satu titik pertemuan berbagai bahasa tersebut.
Karena itulah .NET dapat menyediakan satu ekosistem runtime yang digunakan oleh lebih dari satu bahasa pemrograman.
Pertanyaan Berikutnya
Sampai tahap ini, alur dasarnya sudah mulai terbentuk:
Plain Text
C#↓Compiler↓IL↓.NET Runtime↓Machine Code↓CPU
Namun masih ada bagian penting yang belum dijelaskan.
Di mana sebenarnya IL tersebut disimpan?
Ketika sebuah project C# di-build, mengapa muncul file seperti:
Plain Text
.dll.exe.pdb.deps.json.runtimeconfig.json
Apa isi sebenarnya dari file .dll?
Mengapa .dll yang dihasilkan oleh project .NET tidak sama dengan DLL native yang biasa ditemukan pada sistem operasi?
Dan bagaimana CLR dapat membaca sebuah assembly lalu menemukan class, method, metadata, serta IL di dalamnya?
Untuk menjawab pertanyaan tersebut, pembahasan berikutnya perlu masuk lebih dalam ke konsep assembly.
Bagian 3: C# Bukan .NET
Sebelum melangkah lebih jauh, ada satu konsep yang perlu diluruskan lebih dulu, karena dua istilah ini sering dianggap sama padahal cakupannya sangat berbeda.
C# adalah bahasa pemrograman. Titik. Tugasnya hanya menyediakan syntax dan aturan penulisan kode yang kemudian diterjemahkan oleh compiler menjadi IL, seperti yang sudah dibahas sebelumnya.
.NET jauh lebih luas dari itu. .NET adalah sebuah platform yang di dalamnya terdapat runtime untuk menjalankan aplikasi, Base Class Libraries yang menyediakan fungsi-fungsi siap pakai, SDK untuk keperluan development, CLI sebagai antarmuka command line, berbagai tooling pendukung, dan secara keseluruhan menjadi platform tempat aplikasi dibangun dan dijalankan.
Plain Text
C#│└── Bahasa pemrograman.NET│├── Runtime├── Base Class Libraries├── SDK├── CLI├── Tooling└── Platform aplikasi
Dengan gambaran ini, hubungan keduanya menjadi lebih jelas. C# adalah salah satu bahasa yang dipakai untuk membangun aplikasi .NET, tetapi .NET sendiri tidak identik dengan C#. Justru dari sinilah pertanyaan berikutnya muncul secara alami: kalau .NET bukan hanya soal C#, bahasa apa lagi yang bisa dipakai di atas platform yang sama?
Bagian 4: Mengapa .NET Bisa Menggunakan Banyak Bahasa?
Salah satu karakteristik menarik dari .NET adalah kemampuannya mendukung lebih dari satu bahasa pemrograman. Selain C#, ada juga Visual Basic .NET dan F# yang sama-sama bisa dipakai membangun aplikasi di atas platform ini.
Pertanyaannya, bagaimana bahasa-bahasa dengan syntax dan karakteristik yang jauh berbeda bisa berjalan di runtime yang sama? Jawabannya terletak pada desain .NET yang sudah dibahas sebagian di Bagian 2.
Compiler Berbeda, Runtime Bisa Sama
Perhatikan tiga baris kode berikut yang melakukan hal serupa di tiga bahasa berbeda.
C#:
C#
Console.WriteLine("Hello");
VB.NET:
vbnet
Console.WriteLine("Hello")
F#:
fsharp
printfn "Hello"
Syntax ketiganya jelas berbeda, dan compiler yang memprosesnya juga berbeda satu sama lain. Namun ada satu titik pertemuan yang membuat ketiganya bisa berjalan di atas fondasi yang sama:
Plain Text
C# Source↓C# Compiler↓ILVB.NET Source↓VB Compiler↓ILF# Source↓F# Compiler↓IL
Setelah masing-masing compiler selesai bekerja, hasilnya sama-sama berupa IL. Dari titik itu, alurnya menyatu:
Plain Text
IL↓.NET Runtime↓JIT↓Native Code↓CPU
Runtime Tidak Perlu Memahami Syntax C#
Hal yang menarik dari pola di atas adalah CLR, sebagai bagian dari .NET Runtime, sama sekali tidak perlu tahu apakah IL yang sedang dijalankannya awalnya ditulis dalam C#, VB.NET, atau F#. Yang memahami syntax masing-masing bahasa adalah compiler-nya, bukan runtime. Setelah proses compilation selesai, runtime hanya berurusan dengan IL dan execution model .NET, tanpa peduli bahasa asal kode tersebut.
Pola ini bisa disederhanakan menjadi satu alur:
Plain Text
Different Languages↓Different Compilers↓Common Intermediate Representation↓.NET Runtime
Titik temu di "Common Intermediate Representation" inilah yang menjadi alasan mengapa .NET bisa terasa seperti satu ekosistem, meskipun bahasa yang dipakai di dalamnya bisa berbeda-beda.
Bagian 5: IL / CIL
Istilah IL sudah beberapa kali muncul pada bagian sebelumnya. IL disebut sebagai hasil utama dari proses compilation C#, sebagai bentuk perantara sebelum program akhirnya menjadi machine code.
Namun, apa sebenarnya IL itu?
Untuk memahaminya, perlu dilihat kembali perjalanan sebuah program .NET dari awal:
Plain Text
C# Source Code↓Compiler↓IL↓.NET Runtime↓Native Machine Code↓CPU
IL berada di tengah-tengah perjalanan tersebut. IL bukan lagi source code C#, tetapi juga belum menjadi machine code yang spesifik untuk CPU.
Apa Itu IL?
IL (Intermediate Language) adalah kumpulan instruksi perantara yang dihasilkan oleh compiler .NET dari source code bahasa seperti C#.
Ketika compiler C# memproses kode:
C#
int result = 10 + 20;
compiler tidak langsung menghasilkan instruksi CPU tertentu. Compiler menghasilkan bentuk perantara yang merepresentasikan operasi program tersebut dalam instruction set .NET.
Secara sederhana:
Plain Text
C#││ Compiler▼IL
IL kemudian disimpan di dalam .NET assembly, bersama dengan metadata dan informasi lain yang dibutuhkan runtime. Jadi, ketika sebuah project C# berhasil di-build, hasilnya bukan sekadar kumpulan machine code.
Salah satu bentuk hasil build yang umum adalah:
Plain Text
MyApplication.dll
File tersebut merupakan sebuah .NET assembly yang di dalamnya terdapat IL dan metadata.
Mengapa Disebut Intermediate Language?
Kata intermediate berarti "perantara".
IL disebut demikian karena posisinya berada di antara source code dan native machine code.
Plain Text
Source Language││ C# Compiler▼Intermediate Language││ JIT▼Native Machine Code
C# berada pada tingkat yang dekat dengan cara berpikir manusia.
IL berada pada tingkat yang lebih rendah dan lebih terstruktur, tetapi masih dirancang untuk lingkungan eksekusi .NET. Machine code berada pada tingkat yang jauh lebih dekat dengan hardware.
Dengan demikian, terdapat tiga bentuk kode yang perlu dibedakan:
Plain Text
C#││ Bahasa pemrograman│▼IL / CIL││ Instruksi perantara .NET│▼Machine Code││ Instruksi native CPU│▼CPU
Ketiga istilah tersebut memiliki fungsi yang berbeda.
C# adalah Source Language
C# merupakan source language, yaitu bahasa yang digunakan untuk menulis source code.
Contohnya:
C#
class Calculator{public int Add(int a, int b){return a + b;}}
Kode tersebut dibuat agar dapat dibaca dan ditulis dengan relatif mudah oleh manusia.
C# memiliki berbagai fitur tingkat tinggi seperti:
-
class
-
object
-
method
-
property
-
interface
-
generic
-
exception
-
async/await
-
LINQ
-
pattern matching
-
lambda expression
CPU tidak memahami konsep-konsep tersebut secara langsung.
Compiler C# bertugas menerjemahkan konsep-konsep tersebut ke bentuk yang dapat direpresentasikan dalam IL dan metadata .NET.
IL adalah Intermediate Representation
IL berada satu tingkat di bawah source code.
Misalnya sebuah method C#:
C#
public int Add(int a, int b){return a + b;}
Compiler perlu mengubah method tersebut menjadi serangkaian instruksi IL yang merepresentasikan operasi yang diperlukan.
Secara konseptual, instruksinya dapat dibayangkan seperti:
Plain Text
ambil nilai aambil nilai bjumlahkankembalikan hasil
IL memiliki instruction set sendiri. Instruksi tersebut bukan instruksi x86-64 atau ARM64.
Dengan kata lain:
Plain Text
IL instruction≠x86-64 instruction≠ARM64 instruction
Ketiganya berada pada level yang berbeda.
Machine Code adalah Instruksi CPU
Machine code adalah instruksi native yang sesuai dengan instruction set architecture CPU. Dua CPU dengan arsitektur berbeda dapat memiliki machine code yang berbeda.
Sebagai contoh, program yang sama dapat akhirnya menghasilkan machine code untuk:
Plain Text
x86-64
atau:
Plain Text
ARM64
IL tidak secara langsung menentukan salah satunya.
Itulah salah satu karakteristik penting IL.
Gambaran sederhananya:
Plain Text
┌──► x86-64 Machine Code│C# ──► IL ──► JIT│└──► ARM64 Machine Code
Runtime yang berjalan pada environment tertentu dapat menghasilkan native code yang sesuai dengan environment tersebut.
IL Tidak Dijalankan Langsung oleh CPU
Ini adalah bagian yang sangat penting. CPU tidak membaca IL lalu mengeksekusinya seperti machine code.
Misalnya terdapat IL seperti:
Plain Text
ldarg.0ldarg.1addret
Instruksi tersebut merupakan instruksi pada instruction set IL.
CPU tidak memiliki instruksi bernama ldarg.0 atau ret dalam pengertian IL. CPU membutuhkan native machine instructions.
Karena itu, masih diperlukan proses lain:
Plain Text
IL↓JIT↓Machine Code↓CPU
JIT (Just-In-Time compiler) merupakan salah satu komponen .NET runtime yang dapat menerjemahkan IL menjadi native machine code ketika method perlu dijalankan.
Dengan demikian, alur lengkapnya menjadi:
Plain Text
C# Source Code↓C# Compiler↓.NET Assembly↓IL↓.NET Runtime↓JIT↓Native Machine Code↓CPU
Apa Hubungan IL dengan CIL?
Dalam dokumentasi dan pembahasan .NET, terdapat beberapa istilah yang mungkin terlihat berbeda:
-
IL
-
CIL
-
MSIL
Ketiganya sering digunakan untuk merujuk pada konsep yang sangat berkaitan.
CIL merupakan singkatan dari Common Intermediate Language.
Spesifikasi CLI mendefinisikan CIL sebagai instruction set untuk intermediate language yang digunakan dalam lingkungan Common Language Infrastructure.
Dalam percakapan sehari-hari di dunia .NET, istilah IL jauh lebih umum digunakan.
Karena itu, kode hasil compilation C# sering disebut sebagai:
Plain Text
IL
meskipun secara lebih spesifik dapat disebut:
Plain Text
CIL
Istilah MSIL (Microsoft Intermediate Language) juga sering ditemukan pada dokumentasi atau materi lama. Nama tersebut berasal dari istilah yang digunakan Microsoft pada masa sebelumnya. Untuk pembahasan modern mengenai .NET, istilah IL atau CIL sudah cukup.
IL Memiliki Instruction Set Sendiri
IL bukan sekadar teks yang menjelaskan apa yang harus dilakukan program. IL memiliki instruction set yang dapat direpresentasikan dalam bentuk opcode.
Contoh instruksi IL yang sering ditemukan antara lain:
Plain Text
ldargldlocstlocldstrcallcallvirtnewobjaddsubbrbrtruebrfalseret
Setiap instruksi memiliki fungsi tertentu.
Misalnya:
Plain Text
ldstr
digunakan untuk memuat string.
Plain Text
add
digunakan untuk melakukan operasi penjumlahan.
Plain Text
newobj
digunakan untuk membuat object.
Plain Text
call
digunakan untuk melakukan pemanggilan method tertentu.
Plain Text
ret
digunakan untuk mengembalikan kontrol dari method.
Instruksi-instruksi tersebut kemudian dapat diproses oleh runtime dan JIT.
Contoh Sederhana
Pertimbangkan kode C# berikut:
C#
public static int Add(int a, int b){return a + b;}
Secara konseptual, compiler perlu menghasilkan instruksi yang memiliki arti seperti:
Plain Text
ambil argument pertamaambil argument keduajumlahkankembalikan hasil
Jika IL dari method tersebut dilihat menggunakan tool seperti ILSpy atau ildasm, bentuknya akan menyerupai:
Plain Text
ldarg.0ldarg.1addret
Tidak perlu menghafalkan instruksi tersebut.
Yang penting adalah memahami perjalanan abstraksinya:
Plain Text
C#:return a + b;↓IL:ldarg.0ldarg.1addret↓Native Machine Code:instruksi yang sesuaidengan CPU tertentu
Instruksi IL masih bersifat independen terhadap CPU tertentu.
IL Tidak Hanya Berisi Operasi Sederhana
IL juga mampu merepresentasikan berbagai konstruksi program yang lebih kompleks.
Misalnya object creation:
C#
var person = new Person();
Compiler dapat menghasilkan IL yang menggunakan instruksi seperti:
Plain Text
newobj
Pemanggilan method:
C#
person.GetName();
dapat direpresentasikan menggunakan instruksi pemanggilan seperti:
Plain Text
call
atau:
Plain Text
callvirt
String literal:
C#
"Hello World"
dapat direpresentasikan menggunakan:
Plain Text
ldstr
Percabangan:
C#
if (age >= 18){...}
pada level IL dapat direpresentasikan menggunakan berbagai instruksi branch dan comparison. Dengan demikian, compiler melakukan lebih dari sekadar mengganti kata-kata C# dengan kata-kata IL. Compiler melakukan transformasi dari struktur program tingkat tinggi menjadi representasi instruksi yang dapat dimengerti oleh runtime .NET.
IL dan Metadata
Ada satu hal penting yang perlu diperhatikan: assembly .NET tidak hanya berisi IL. Assembly juga memiliki metadata.
Metadata berisi informasi mengenai struktur program, misalnya:
Plain Text
TypeMethodFieldPropertyParameterAssembly Reference
Misalnya terdapat class:
C#
public class Person{public string Name { get; set; }public void SayHello(){Console.WriteLine(Name);}}
Runtime tidak hanya membutuhkan instruksi untuk method SayHello.
Runtime juga perlu mengetahui bahwa:
Plain Text
Person├── Property: Name└── Method: SayHello
Informasi struktural tersebut merupakan bagian dari metadata.
Karena itu, gambaran assembly menjadi:
Plain Text
.NET Assembly│├── IL│├── Metadata│├── Type Information│└── References
IL dan metadata bekerja bersama untuk membentuk informasi yang dibutuhkan runtime.
Runtime Optimization
IL memungkinkan runtime melakukan compilation menuju native code pada environment tempat program benar-benar dijalankan.
JIT dapat mengetahui target environment dan menghasilkan machine code yang sesuai.
Pemisahan Tanggung Jawab
Compiler dan runtime dapat memiliki tanggung jawab yang berbeda.
Compiler:
Plain Text
C# → IL
Runtime/JIT:
Plain Text
IL → Machine Code
Pemisahan ini membuat arsitektur .NET menjadi lebih fleksibel.
IL Tidak Berarti Aplikasi Selalu Portable Tanpa Batas
Penting untuk tidak menarik kesimpulan terlalu jauh. Adanya IL tidak berarti semua aplikasi .NET otomatis dapat dijalankan di semua sistem operasi tanpa perubahan. Aplikasi dapat memiliki dependency yang spesifik terhadap platform tertentu. Contohnya, sebuah aplikasi dapat menggunakan API native Windows tertentu. Dalam kasus tersebut, IL memang bersifat portable, tetapi dependency aplikasi belum tentu portable.
Selain itu, aplikasi .NET juga dapat bergantung pada runtime version, native library, operating system API, atau konfigurasi tertentu. Jadi, IL memberikan fondasi portability, bukan jaminan bahwa seluruh aplikasi selalu portable.
Bagian 6: Common Type System (CTS)
Pada bagian sebelumnya telah dijelaskan bahwa C#, Visual Basic, dan F# dapat menghasilkan IL yang berjalan di atas .NET. Hal ini menimbulkan sebuah pertanyaan penting:
Jika setiap bahasa memiliki syntax dan aturan masing-masing, bagaimana tipe data dari bahasa yang berbeda dapat tetap dipahami secara konsisten oleh .NET?
Misalnya, C# memiliki syntax:
C#
int age = 26;
Sementara bahasa lain dapat menggunakan syntax yang berbeda untuk menyatakan konsep yang sama. Jika setiap bahasa memiliki definisi tipe yang sepenuhnya berbeda, runtime akan kesulitan memahami hubungan antara tipe-tipe tersebut.
.NET menyelesaikan persoalan ini melalui sebuah fondasi yang disebut Common Type System, atau CTS.
Apa Itu Common Type System?
Common Type System (CTS) adalah seperangkat aturan yang mendefinisikan bagaimana tipe didefinisikan, direpresentasikan, dan digunakan di dalam lingkungan .NET. CTS menyediakan fondasi type system yang digunakan oleh berbagai bahasa yang berjalan di atas .NET.
Dengan adanya CTS, sebuah tipe tidak hanya memiliki arti berdasarkan syntax bahasa sumbernya. Tipe tersebut juga memiliki representasi dan aturan yang dipahami oleh ekosistem .NET.
Secara sederhana:
Plain Text
C# ─────────┐│VB.NET ─────┼──► CTS│F# ─────────┘
Masing-masing bahasa tetap memiliki syntax dan fitur sendiri, tetapi tipe-tipe yang dihasilkan dapat dipetakan ke konsep yang didefinisikan oleh CTS.
Mengapa CTS Dibutuhkan?
Tanpa type system bersama, setiap bahasa dapat memiliki cara sendiri dalam mendefinisikan tipe.
Misalnya secara konseptual:
Plain Text
Bahasa A└── Integer versi ABahasa B└── Integer versi BBahasa C└── Integer versi C
Jika ketiganya benar-benar berbeda pada level runtime, sebuah method yang menerima integer dari Bahasa A belum tentu dapat menerima integer dari Bahasa B.
.NET menginginkan model yang lebih terintegrasi.
Dengan CTS, berbagai bahasa dapat memiliki konsep tipe yang dipetakan ke fondasi yang sama:
Plain Text
C# Integer ─────┐│VB Integer ─────┼──► CTS Type System│F# Integer ─────┘
Dengan demikian, runtime memiliki aturan yang konsisten untuk memahami tipe tersebut.
CTS Tidak Mengatur Syntax Bahasa
CTS tidak menentukan bagaimana syntax C# harus ditulis.
Sebagai contoh, CTS tidak mengatakan bahwa sebuah integer harus ditulis seperti:
C#
int age = 26;
Itu merupakan aturan bahasa C#.
Bahasa lain dapat menggunakan syntax berbeda.
Perbedaannya dapat digambarkan seperti ini:
Plain Text
C# Syntax│▼Compiler C#│▼CTS-compatible type
dan:
Plain Text
VB.NET Syntax│▼VB Compiler│▼CTS-compatible type
Jadi, CTS berada di level yang lebih fundamental daripada syntax bahasa.
Bahasa menentukan cara programmer menulis kode, sedangkan CTS menentukan fondasi bagaimana tipe tersebut dipahami dalam ekosistem .NET.
Contoh: Integer
Salah satu contoh paling mudah adalah integer.
Dalam C# dapat ditulis:
C#
int number = 10;
int merupakan keyword C# yang digunakan untuk menyatakan tipe integer 32-bit bertanda.
Di balik syntax tersebut, C# memetakan int ke tipe .NET:
Plain Text
System.Int32
Jadi:
C#
int
dan:
Plain Text
System.Int32
merujuk pada tipe yang sama.
Contoh:
C#
int number = 10;System.Int32 anotherNumber = 20;
Keduanya menggunakan System.Int32.
Hal ini menunjukkan bahwa keyword bahasa seperti int tidak selalu merupakan nama fundamental dari tipe pada level .NET.
int adalah alias yang disediakan C# untuk System.Int32.
Contoh Tipe Lain
Hal yang sama berlaku pada banyak tipe dasar lainnya.
Beberapa contoh umum:
| C# | .NET Type |
|---|---|
int | System.Int32 |
long | System.Int64 |
short | System.Int16 |
byte | System.Byte |
bool | System.Boolean |
char | System.Char |
float | System.Single |
double | System.Double |
decimal | System.Decimal |
string | System.String |
object | System.Object |
Contohnya:
C#
string name = "Haekal";
secara fundamental menggunakan:
Plain Text
System.String
Sedangkan:
C#
object value = 10;
menggunakan:
Plain Text
System.Object
Jadi keyword yang terlihat di source code C# merupakan bagian dari bahasa C#, sedangkan tipe yang mendasarinya merupakan bagian dari type system .NET.
CTS Mendefinisikan Berbagai Kategori Tipe
CTS tidak hanya mendefinisikan tipe seperti integer dan string. CTS juga mendefinisikan konsep yang lebih luas tentang berbagai jenis tipe yang dapat ada di lingkungan .NET.
Beberapa konsep penting di antaranya adalah:
-
value type
-
reference type
-
class
-
interface
-
struct
-
enum
-
delegate
-
array
-
built-in type
Konsep-konsep tersebut menjadi bagian dari fondasi type system .NET.
Misalnya C# memiliki:
C#
struct Point{public int X;public int Y;}
dan:
C#
class Person{public string Name { get; set; }}
Keduanya menggunakan konsep type yang berbeda. Point merupakan value type, sedangkan Person merupakan reference type. CTS mendefinisikan aturan fundamental mengenai perbedaan tersebut.
Value Type dan Reference Type
Salah satu konsep penting dalam CTS adalah pembagian tipe menjadi value type dan reference type.
Secara sederhana:
Plain Text
CTS│├── Value Type│└── Reference Type
Value Type
Value type secara konseptual menyimpan nilainya secara langsung.
Contohnya:
C#
int age = 26;
int merupakan value type.
Struct juga merupakan value type:
C#
struct Point{public int X;public int Y;}
Begitu pula enum:
C#
enum Status{Pending,Completed}
CTS menentukan karakteristik fundamental value type tersebut.
Reference Type
Reference type digunakan untuk tipe yang merepresentasikan object dan reference terhadap object tersebut.
Contohnya:
C#
class Person{public string Name { get; set; }}
Person merupakan reference type.
Beberapa tipe reference type lainnya meliputi:
Plain Text
classinterfacedelegatearraystringobject
Pembagian value type dan reference type merupakan bagian penting dari bagaimana .NET memahami object dan nilai.
Semua Tipe Memiliki Fondasi yang Sama
Salah satu konsep penting dalam CTS adalah adanya hierarki type system.
Pada tingkat paling dasar terdapat:
Plain Text
System.Object
Banyak reference type pada .NET pada akhirnya memiliki hubungan dengan System.Object.
Misalnya:
C#
class Person{}
secara konsep memiliki hubungan dengan:
Plain Text
System.Object↑Person
Hal ini memungkinkan konsep seperti polymorphism dan casting bekerja secara konsisten.
Contohnya:
C#
object value = "Hello";
string merupakan reference type dan dapat diperlakukan sebagai object.
Kemudian:
C#
object value = 123;
juga dapat dilakukan.
Dalam kasus integer, terjadi proses boxing, karena int merupakan value type yang ditempatkan dalam bentuk object. Konsep tersebut merupakan bagian dari type system .NET dan bukan sekadar fitur syntax C#.
CTS dan Class
CTS juga mendefinisikan konsep class sebagai salah satu bentuk reference type.
Dalam C#:
C#
class Person{public string Name { get; set; }}
class tersebut memiliki:
-
nama type;
-
property;
-
method;
-
field;
-
dan anggota lainnya.
Bahasa lain yang berjalan di atas .NET dapat memiliki cara berbeda untuk mendefinisikan sebuah class, tetapi hasil akhirnya harus mengikuti aturan type system yang dapat dipahami oleh runtime.
Dengan demikian, konsep class tidak hanya milik syntax C#.
C# memiliki keyword:
C#
class
tetapi konsep class pada level .NET merupakan bagian dari CTS.
CTS dan Interface
CTS juga mendukung konsep interface.
Dalam C#:
C#
interface IAnimal{void Speak();}
Interface mendefinisikan kontrak yang dapat diimplementasikan oleh class.
Contohnya:
C#
class Dog : IAnimal{public void Speak(){Console.WriteLine("Bark");}}
Konsep interface tersebut tidak hanya penting bagi C#, tetapi merupakan bagian dari model type system yang dapat dipahami oleh .NET.
Karena itu, bahasa lain yang mendukung konsep interface dapat menghasilkan type yang tetap dapat digunakan dalam ekosistem .NET.
CTS dan Struct
Struct merupakan bentuk value type.
Dalam C#:
C#
struct Point{public int X;public int Y;}
Berbeda dengan class:
C#
class Point{public int X;public int Y;}
keduanya memiliki syntax yang mirip, tetapi karakteristik type-nya berbeda.
CTS mendefinisikan bahwa struct merupakan value type, sedangkan class merupakan reference type. Ini merupakan contoh bagaimana CTS menyediakan aturan yang lebih fundamental daripada syntax bahasa.
CTS dan Enum
Enum juga merupakan bagian dari type system .NET.
Contoh:
C#
enum OrderStatus{Pending,Paid,Shipped}
Enum memungkinkan sekumpulan nilai bernama direpresentasikan sebagai sebuah type.
CTS menentukan karakteristik enum tersebut sehingga runtime dan bahasa .NET lainnya dapat memahami bahwa type tersebut merupakan enum, bukan sekadar kumpulan constant biasa.
CTS dan Delegate
Delegate merupakan konsep lain yang didukung oleh CTS.
Dalam C#:
C#
delegate void MessageHandler(string message);
Delegate dapat digunakan untuk merepresentasikan reference terhadap method dengan signature tertentu.
Contohnya:
C#
void PrintMessage(string message){Console.WriteLine(message);}
Kemudian method tersebut dapat digunakan melalui delegate yang sesuai.
Konsep delegate menjadi bagian dari type system .NET sehingga dapat direpresentasikan dalam metadata dan digunakan oleh runtime.
CTS Tidak Membuat Semua Bahasa Menjadi Sama
Penting untuk memahami batasan CTS.
CTS tidak berarti C#, VB.NET, dan F# memiliki syntax yang sama.
Setiap bahasa tetap memiliki karakteristik sendiri. Misalnya C# memiliki fitur dan aturan syntax tertentu yang tidak harus tersedia dalam bahasa lain.
Gambaran yang lebih tepat adalah:
Plain Text
C#││CTS││.NET Type System│┌─────┴─────┐│ │Runtime Libraries
Bahasa tetap berbeda pada level syntax dan fitur bahasa.
Namun ketika type tersebut masuk ke ekosistem .NET, terdapat fondasi bersama yang memungkinkan runtime memahami type tersebut.
CTS sebagai Kontrak Type System
CTS dapat dipandang sebagai sebuah kontrak bersama mengenai tipe.
Misalnya terdapat sebuah type:
Plain Text
Person
Runtime perlu memiliki cara yang konsisten untuk mengetahui:
Plain Text
Nama typeBase typeMethodPropertyFieldInterfaceAccessibilityParameterReturn type
CTS menyediakan aturan mengenai bagaimana informasi tersebut direpresentasikan dalam type system .NET. Dengan begitu, runtime tidak perlu memahami setiap bahasa secara terpisah.
Runtime tidak perlu memiliki aturan seperti:
Plain Text
Jika kode berasal dari C#, lakukan X.Jika kode berasal dari VB.NET, lakukan Y.Jika kode berasal dari F#, lakukan Z.
Sebaliknya, setelah compiler masing-masing bahasa menghasilkan representasi yang sesuai dengan aturan .NET, runtime dapat bekerja berdasarkan type system yang sama.
Hubungan CTS dengan Compiler
Setiap bahasa memiliki compiler sendiri.
Secara konseptual:
Plain Text
C# Source↓C# Compiler↓CTS-compatible .NET Types↓IL + Metadata
Bahasa lain:
Plain Text
VB.NET Source↓VB Compiler↓CTS-compatible .NET Types↓IL + Metadata
Dan:
Plain Text
F# Source↓F# Compiler↓CTS-compatible .NET Types↓IL + Metadata
Compiler bertanggung jawab menerjemahkan syntax bahasa masing-masing ke bentuk yang sesuai dengan type system dan runtime .NET. Inilah yang memungkinkan satu runtime menangani program yang berasal dari berbagai bahasa.
CTS Bukan CLR
Dua istilah ini sering muncul berdekatan, tetapi memiliki peran yang berbeda. CTS adalah aturan mengenai type system. CLR adalah runtime yang menjalankan managed code dan menyediakan berbagai layanan runtime.
Secara sederhana:
Plain Text
CTS││ mendefinisikan aturan tipe▼.NET Type SystemCLR││ menjalankan managed code▼.NET Runtime
CTS menjelaskan bagaimana tipe didefinisikan dan berperilaku dalam ekosistem .NET. CLR bertugas menjalankan program yang menggunakan tipe-tipe tersebut. Keduanya saling berkaitan, tetapi bukan komponen yang sama.
CTS Bukan C# Type System
C# memiliki aturan type system yang lebih spesifik sebagai sebuah bahasa.
Sebagai contoh, C# memiliki fitur seperti:
C#
vardynamicnullable reference typespattern matching
Tidak semua detail tersebut merupakan bagian dari CTS dengan cara yang sama.
CTS berada pada level fondasi yang lebih umum.
Hubungannya dapat digambarkan:
Plain Text
C# Type System│▼Common Type System│▼.NET Runtime
C# dapat menyediakan fitur tambahan di atas fondasi tersebut selama compiler dapat menerjemahkan dan merepresentasikannya dengan cara yang sesuai dengan runtime dan metadata .NET.
Batasan Interoperability
CTS memang memungkinkan bahasa-bahasa .NET berbagi fondasi type system, tetapi bukan berarti semua fitur setiap bahasa dapat digunakan tanpa batas di bahasa lain. Misalnya sebuah bahasa dapat memiliki fitur yang tidak memiliki representasi langsung atau nyaman untuk bahasa lain.
Karena itu, terdapat perbedaan antara:
dapat direpresentasikan oleh .NET
dan:
dapat digunakan dengan nyaman oleh semua bahasa .NET.
CTS menyelesaikan masalah fondasi type system, tetapi interoperability juga dipengaruhi oleh compiler, metadata, aturan bahasa, dan fitur khusus masing-masing bahasa.
Gambaran Besar
Setelah memahami CTS, hubungan antara beberapa komponen .NET menjadi lebih jelas:
Plain Text
C# ──────────┐│VB.NET ──────┼──► Compiler│ │F# ──────────┘ ▼IL+Metadata│▼CTS│▼.NET Runtime
CTS menjadi fondasi yang memastikan bahwa konsep tipe dalam ekosistem .NET memiliki aturan bersama.
C# boleh menggunakan keyword:
C#
int
VB.NET dapat memiliki syntax berbeda untuk integer.
F# juga memiliki syntax sendiri.
Namun pada level .NET, tipe tersebut dapat dipetakan ke fondasi type system yang sama, seperti:
Plain Text
System.Int32
Begitu pula dengan konsep:
Plain Text
classinterfacestructenumdelegatearrayobjectstring
Konsep-konsep tersebut memiliki representasi dan aturan yang dapat dipahami oleh ekosistem .NET.
Bagian 7: Interoperability Antar Bahasa
Manfaat dari CTS dan CLS akan lebih terasa lewat contoh konkret. Misalnya ada sebuah library bernama MyLibrary.dll yang dibuat menggunakan C#.
Selama API dan dependensinya mengikuti aturan kompatibilitas yang sudah dibahas sebelumnya, library tersebut bisa dipakai oleh aplikasi yang ditulis dalam bahasa .NET lain, bukan hanya oleh aplikasi C#.
Plain Text
C# Library↓MyLibrary.dll↓┌──────────┬──────────┐│ │ │C# VB.NET F#
Hal yang sama berlaku sebaliknya. Sebuah library yang ditulis dalam F# juga bisa dipakai oleh aplikasi C#, selama kondisinya memenuhi aturan kompatibilitas tadi.
Kemampuan seperti inilah yang membuat ekosistem .NET tidak terpecah menjadi runtime yang berbeda-beda untuk setiap bahasa. Semua bahasa berbagi fondasi tipe dan aturan kompatibilitas yang sama, sehingga library bisa saling dipakai lintas bahasa tanpa hambatan berarti.
Sampai titik ini, pembahasan baru sampai pada IL sebagai hasil compilation. Namun IL sendiri tidak pernah berdiri sendirian begitu saja. Ia selalu dibungkus dalam sesuatu yang lebih besar, dan itulah yang akan dibahas berikutnya.
Bagian 7: Apa Itu Assembly?
Setelah proses compilation selesai, yang terlihat di folder project bukan file bernama IL. Hasil build biasanya berupa file dengan ekstensi yang sudah familiar, seperti:
Plain Text
MyApp.dll
atau:
Plain Text
MyApp.exe
Lalu muncul pertanyaan:
Apa sebenarnya isi dari file-file tersebut?
Jawabannya adalah assembly.
Assembly merupakan salah satu unit penting dalam .NET. Assembly menjadi wadah hasil compilation sekaligus membawa berbagai informasi yang dibutuhkan oleh .NET runtime untuk memahami, memuat, dan menjalankan kode.
Secara sederhana:
Plain Text
C# Source Code││ Compiler▼.NET Assembly│├── IL├── Metadata├── Manifest├── Type Information├── References└── Resources
Assembly dapat dikatakan sebagai bentuk hasil compilation yang siap digunakan oleh ekosistem .NET.
Assembly Bukan Sekadar DLL
Ada perbedaan penting antara file fisik dan konsep assembly. DLL dan EXE adalah bentuk file yang terlihat di file system. Sedangkan assembly adalah konsep .NET yang menggambarkan unit deployment dan identitas yang berisi IL, metadata, manifest, serta resource yang berkaitan.
Dengan kata lain:
Plain Text
.dll ──► dapat menjadi sebuah .NET Assembly.exe ──► dapat menjadi sebuah .NET Assembly
Jadi, istilah DLL dan assembly tidak sepenuhnya sinonim. DLL adalah format file. Assembly adalah unit logis dalam .NET. Sebuah assembly dapat dikemas dalam file .dll maupun .exe.
Apa Perbedaan DLL dan EXE?
Dalam konteks .NET modern, baik .dll maupun .exe dapat berisi IL dan metadata. Perbedaan utamanya adalah cara file tersebut dimaksudkan untuk digunakan.
DLL
DLL merupakan singkatan dari Dynamic Link Library. Dalam aplikasi .NET, file .dll umumnya digunakan sebagai library, yaitu kumpulan kode yang digunakan oleh aplikasi lain atau oleh komponen .NET lainnya.
Contohnya sebuah project library:
Plain Text
MyLibrary│▼MyLibrary.dll
Assembly tersebut dapat berisi:
C#
public class Calculator{public int Add(int a, int b){return a + b;}}
Aplikasi lain kemudian dapat mereferensikan assembly tersebut:
Plain Text
MyApp│├── MyApp.dll│└── MyLibrary.dll
MyApp dapat menggunakan type yang tersedia di MyLibrary.dll. Jadi, fungsi utama DLL dalam konteks ini adalah menyediakan kode atau komponen yang dapat digunakan oleh aplikasi lain.
Namun, ada satu hal penting:
DLL .NET tidak berarti file tersebut tidak dapat dijalankan sama sekali.
Sebuah DLL dapat berisi entry point dalam kondisi tertentu, misalnya hasil build aplikasi .NET yang menghasilkan assembly .dll sebagai bentuk utama aplikasi. Assembly tersebut dapat dijalankan menggunakan dotnet, selama memang merupakan executable assembly yang memiliki entry point dan runtime configuration yang sesuai. Jadi, istilah "DLL = tidak bisa dijalankan" terlalu sederhana untuk .NET modern.
EXE
EXE merupakan singkatan dari executable. File .exe secara umum digunakan untuk sesuatu yang dimaksudkan sebagai executable application.
Misalnya:
Plain Text
MyApp.exe
File tersebut dapat menjadi entry point aplikasi yang dijalankan pada Windows. Dalam model .NET tertentu, executable .NET dapat berupa PE file yang berisi IL dan metadata, bukan berarti seluruh isinya sudah berupa native machine code.
Dengan demikian:
Plain Text
MyApp.exe│├── IL├── Metadata├── Manifest└── Entry Poin
Ketika aplikasi dijalankan, .NET runtime dapat memuat assembly tersebut dan kemudian menjalankan entry point-nya.
DLL dan EXE Sama-Sama Dapat Menjadi Assembly
Perbedaan ini menjadi lebih jelas jika dilihat seperti berikut:
Plain Text
.NET Assembly│┌────────┴────────┐│ │.dll .exe│ │Library / ExecutableComponent Application
Keduanya dapat memiliki:
Plain Text
ILMetadataManifestType InformationReferencesResources
Yang membedakan terutama adalah peran dan cara assembly tersebut digunakan. DLL biasanya digunakan sebagai library atau dependency. EXE biasanya digunakan sebagai executable application.
Contoh Struktur Aplikasi
Misalnya terdapat aplikasi sederhana:
Plain Text
MyApp/│├── MyApp.dll├── MyApp.exe├── MyApp.deps.json├── MyApp.runtimeconfig.json└── MyApp.pdb
Pada aplikasi .NET modern, file-file tersebut memiliki fungsi yang berbeda.
MyApp.dll
Berisi assembly utama aplikasi, termasuk IL dan metadata.
MyApp.exe
Pada target tertentu, menjadi executable launcher atau executable entry point yang digunakan untuk menjalankan aplikasi.
MyApp.deps.json
Menyimpan informasi dependency aplikasi, termasuk dependency library yang dibutuhkan.
MyApp.runtimeconfig.json
Menyimpan informasi mengenai runtime .NET yang dibutuhkan aplikasi.
MyApp.pdb
Berisi debugging symbols yang membantu debugger menghubungkan hasil compilation dengan source code.
Tidak semua jenis project menghasilkan kombinasi file yang persis sama. Bentuk output bergantung pada target framework, runtime, jenis aplikasi, dan konfigurasi publish.
DLL .NET Tidak Sama dengan DLL Native
Istilah DLL sudah digunakan jauh sebelum .NET. Pada Windows, DLL native biasanya berisi machine code yang ditujukan untuk arsitektur tertentu. Misalnya sebuah native DLL x64 dapat berisi instruksi machine code x86-64.
.NET DLL memiliki karakteristik berbeda.
Sebuah .NET DLL biasanya berisi:
Plain Text
.NET DLL│├── IL├── Metadata├── Manifest└── Resources
Bukan berarti tidak ada native code sama sekali dalam ekosistem .NET. .NET juga dapat berinteraksi dengan native libraries dan dapat menghasilkan native executable melalui teknologi seperti Native AOT.
Namun untuk model managed .NET yang umum:
Plain Text
.dll│└── IL + Metadata│▼.NET Runtime│▼JIT│▼Machine Code
Karena itu, sebuah .dll hasil build aplikasi .NET tidak boleh langsung diasumsikan sebagai native machine code.
Assembly Memiliki Identitas
Assembly bukan hanya kumpulan IL. Assembly juga memiliki identitas.
Secara umum, identitas assembly mencakup informasi seperti:
Plain Text
Assembly NameVersionCulturePublic Key / Strong Name Information
Informasi tersebut digunakan .NET untuk membedakan dan mengidentifikasi assembly.
Misalnya terdapat:
Plain Text
MyLibraryVersion 1.0.0.0
dan:
Plain Text
MyLibraryVersion 2.0.0.0
Keduanya dapat merupakan assembly dengan nama yang sama tetapi versi berbeda. Informasi mengenai assembly tersebut menjadi bagian dari metadata dan manifest.
Manifest
Salah satu bagian penting dari assembly adalah manifest. Manifest berisi informasi yang menggambarkan assembly itu sendiri.
Secara sederhana:
Plain Text
Assembly│├── Manifest│ ├── Assembly Name│ ├── Version│ ├── Culture│ ├── References│ └── Other Assembly Metadata│├── IL├── Type Metadata└── Resources
Manifest membantu runtime mengetahui identitas assembly dan dependency yang terkait dengannya.
Misalnya sebuah assembly menggunakan assembly lain:
Plain Text
MyApp.dll│├── menggunakan MyLibrary.dll└── menggunakan System.Text.Json
Informasi mengenai reference tersebut dapat direpresentasikan dalam metadata assembly.
References
Sebuah assembly hampir tidak pernah berdiri sendirian. Sebuah aplikasi biasanya menggunakan berbagai library.
Contohnya:
Plain Text
MyApp.dll│├── MyLibrary.dll├── System.Text.Json└── Other Dependency
Assembly menyimpan informasi mengenai dependency atau reference yang digunakannya. Runtime kemudian dapat menggunakan informasi tersebut ketika memuat aplikasi dan dependency yang diperlukan.
Resources
Assembly juga dapat membawa resources. Resources dapat berupa data yang bukan merupakan IL secara langsung, misalnya:
Plain Text
ImagesStringsLocalization ResourcesEmbedded Files
Contohnya aplikasi dapat memiliki resource untuk beberapa bahasa:
Plain Text
Resources├── English├── Indonesian└── Japanese
Resource tersebut dapat dikemas bersama assembly sehingga aplikasi dapat mengaksesnya saat runtime.
Assembly sebagai Unit Deployment
Assembly juga penting karena menjadi salah satu unit deployment dalam .NET. Misalnya sebuah aplikasi terdiri dari:
Plain Text
MyApp.dllMyLibrary.dllLogging.dllDatabase.dll
Setiap file dapat menjadi assembly yang berbeda.
Hubungannya:
Plain Text
Application│├── MyApp.dll│├── MyLibrary.dll│├── Logging.dll│└── Database.dll
Runtime dapat memuat assembly-assembly tersebut sesuai kebutuhan aplikasi. Dengan model ini, sebuah aplikasi tidak harus memiliki seluruh kode dalam satu file. Kode dapat dipisahkan menjadi beberapa library dengan tanggung jawab yang berbeda. Hal ini juga mendukung struktur aplikasi yang lebih modular.
DLL sebagai Library, EXE sebagai Application
Perbedaan paling praktis dapat diringkas sebagai berikut:
.dll | .exe | |
|---|---|---|
| Kepanjangan | Dynamic Link Library | Executable |
| Peran umum | Library / dependency | Executable application |
| Dapat berisi IL | Ya | Ya |
| Dapat berisi metadata | Ya | Ya |
| Dapat menjadi .NET assembly | Ya | Ya |
| Umumnya memiliki entry point aplikasi | Tidak | Ya |
| Umumnya digunakan oleh | Aplikasi atau library lain | User / OS / runtime |
Kolom terakhir perlu dipahami sebagai penggunaan umum, bukan aturan mutlak. Yang paling penting adalah membedakan bentuk file dari peran assembly.
Bagaimana dengan dotnet run?
Pada aplikasi .NET modern, perintah:
Bash
dotnet run
tidak berarti compiler C# menghasilkan sebuah file executable native lalu CPU langsung menjalankannya.
Secara konseptual, prosesnya lebih dekat dengan:
Plain Text
Source Code↓Build↓.NET Assembly↓.NET Runtime↓Execution
Untuk aplikasi tertentu, hasil build utama dapat berupa:
Plain Text
MyApp.dll
Kemudian aplikasi dijalankan melalui host dotnet:
Bash
dotnet MyApp.dll
Di sini dotnet bertindak sebagai host yang memulai .NET runtime dan meminta runtime menjalankan assembly tersebut.
Pada konfigurasi lain, terutama deployment tertentu di Windows, dapat tersedia:
Plain Text
MyApp.exe
yang dapat digunakan sebagai executable aplikasi.
Jadi, .dll dan .exe tidak sebaiknya dipahami hanya sebagai:
Plain Text
.dll = library.exe = program
Dalam .NET modern, hubungan keduanya lebih fleksibel.
Native Executable dan Managed Executable
Perbedaan ini juga penting untuk memahami apa yang sebenarnya terjadi saat aplikasi dijalankan. Sebuah executable native yang dikompilasi menggunakan compiler native dapat berisi machine code yang siap dijalankan CPU.
Sementara executable .NET dalam model managed code dapat berisi IL dan membutuhkan .NET runtime untuk menjalankan managed code tersebut.
Secara sederhana:
Plain Text
Native Application│▼Machine Code│▼CPU
Sedangkan:
Plain Text
.NET Application│▼IL + Metadata│▼.NET Runtime│▼Native Machine Code│▼CPU
Model ini kembali menghubungkan pembahasan assembly dengan IL yang telah dijelaskan sebelumnya.
Bagian 8: Metadata
Salah satu alasan metadata menjadi bagian penting dalam assembly adalah karena metadata tersebut dapat dibaca kembali ketika aplikasi sedang berjalan.
.NET menyediakan mekanisme bernama Reflection yang memungkinkan program membaca informasi mengenai assembly dan type secara dinamis. Dengan Reflection, program dapat mengetahui informasi seperti nama class, property, method, field, constructor, interface, hingga attribute yang terdapat pada sebuah assembly.
Sebagai contoh, ketika terdapat class:
C#
public class Person{public string Name { get; set; }public void SayHello(){Console.WriteLine(Name);}}
informasi mengenai Person, Name, dan SayHello() tidak hanya digunakan oleh compiler. Informasi tersebut juga direpresentasikan dalam metadata assembly sehingga dapat dibaca kembali oleh runtime melalui Reflection.
Secara sederhana:
Plain Text
Source Code↓Compiler↓Assembly┌───────────────┐│ IL ││ Metadata ││ Type Info ││ Attributes │└───────┬───────┘│▼Reflection│▼Membaca strukturprogram saat runtime
Dengan Reflection, sebuah program bahkan dapat memeriksa assembly yang tidak diketahui ketika source code ditulis. Misalnya, aplikasi plugin dapat memuat sebuah .dll, kemudian mencari class tertentu, memeriksa interface yang diimplementasikan, menemukan method, dan membuat instance secara dinamis.
Pembahasan lebih lengkap mengenai cara kerja dan penggunaan Reflection tersedia dalam artikel:
Memahami Reflection di C#: Cara Kerja, Penggunaan, dan Contoh Implementasi
Assembly dan Attribute
Metadata juga berkaitan dengan Attribute.
Attribute memungkinkan informasi tambahan ditempelkan pada class, method, property, parameter, assembly, dan berbagai elemen kode lainnya.
Contohnya:
C#
[Obsolete("Gunakan method baru")]public void OldMethod(){}
Obsolete bukan sekadar komentar. Attribute tersebut menjadi bagian dari informasi yang dapat diketahui oleh compiler dan juga dapat direpresentasikan dalam metadata assembly.
Karena itu, secara konseptual:
Plain Text
[Obsolete]│▼Compiler│▼Assembly Metadata│▼Reflection│▼Attribute dapat dibacasaat runtime
Hubungan antara Attribute dan Reflection sangat erat. Attribute dapat digunakan untuk memberikan informasi deklaratif pada kode, sedangkan Reflection dapat digunakan untuk membaca informasi tersebut ketika aplikasi berjalan.
Misalnya sebuah framework dapat mencari semua class yang memiliki attribute tertentu, kemudian menggunakan informasi tersebut untuk menentukan bagaimana class tersebut harus diproses.
Pembahasan mengenai Attribute dan bagaimana konsep tersebut digunakan dalam C# dapat dilanjutkan melalui artikel terkait:
Memahami Attribute di C#: Metadata yang Membuat Kode Lebih Fleksibel dan Powerful
Dengan demikian, metadata bukan sekadar informasi tambahan yang tersimpan di dalam assembly. Metadata menjadi salah satu fondasi yang memungkinkan berbagai kemampuan .NET bekerja secara dinamis, termasuk Reflection, Attribute, tooling, serialization, dependency injection, dan berbagai framework lainnya.
Hubungan sederhananya dapat dirangkum sebagai:
Plain Text
C# Source Code↓Compiler↓.NET Assembly│├── IL│└── Metadata│├── Type Information├── Method Information├── Property Information└── Attribute Information│▼Reflection
Inilah alasan pembahasan assembly tidak berhenti pada IL. IL menjelaskan instruksi program, sedangkan metadata menjelaskan struktur program. Keduanya berada di dalam assembly dan bersama-sama memberikan informasi yang dibutuhkan oleh ekosistem .NET.
Bagian 9: Apa Itu .NET Runtime?
Pada bagian sebelumnya, proses compilation menghasilkan sebuah file assembly seperti MyApp.dll. File tersebut sudah berisi IL (Intermediate Language) dan metadata yang menjelaskan berbagai informasi tentang program, seperti tipe, method, field, dan referensi terhadap assembly lain.
Namun, MyApp.dll belum menjadi program yang dapat langsung dijalankan oleh CPU. IL bukanlah instruksi mesin yang secara langsung dipahami oleh prosesor. Assembly masih membutuhkan sebuah environment yang bertugas memuatnya, menyediakan berbagai layanan runtime, menerjemahkan IL menjadi instruksi mesin, mengelola memory, menangani exception, dan melakukan berbagai pekerjaan lain selama program berjalan. Environment tersebut disebut .NET Runtime.
Secara konseptual, posisi .NET Runtime berada setelah assembly:
Plain Text
Source Code↓C# Compiler↓Assembly (.dll / .exe)↓.NET Runtime↓Execution
.NET Runtime dapat dipandang sebagai lingkungan eksekusi yang menyediakan berbagai layanan agar kode .NET dapat benar-benar berjalan.
Assembly Bukan Program yang Langsung Dieksekusi CPU
Hal penting yang perlu dipahami adalah bahwa file seperti:
Plain Text
MyApp.dll
bukan sekadar kumpulan instruksi CPU.
Di dalamnya terdapat IL dan metadata. Sebagai contoh, kode C# sederhana:
C#
int Add(int a, int b){return a + b;}
tidak langsung berubah menjadi instruksi khusus untuk prosesor Intel atau ARM ketika dikompilasi menjadi assembly. Compiler menghasilkan IL yang bersifat relatif independen terhadap arsitektur CPU.
Secara sederhana, alurnya dapat digambarkan seperti ini:
Plain Text
C# Source Code↓Compiler↓IL↓MyApp.dll↓.NET Runtime↓Machine Code↓CPU
Pendekatan ini merupakan salah satu alasan mengapa aplikasi .NET dapat berjalan pada berbagai sistem operasi dan arsitektur CPU. Assembly yang sama secara konsep dapat dieksekusi pada environment yang berbeda selama tersedia runtime yang sesuai.
Namun, .NET Runtime tidak hanya bertugas mengubah IL menjadi machine code. Selama aplikasi berjalan, terdapat banyak pekerjaan lain yang harus dilakukan.
Apa Saja yang Dilakukan .NET Runtime?
.NET Runtime menyediakan berbagai layanan yang dibutuhkan aplikasi selama execution. Beberapa layanan penting tersebut antara lain:
Plain Text
.NET Runtime│├── Assembly Loading├── Type System├── JIT Compilation├── Memory Management├── Garbage Collection├── Exception Handling├── Threading├── Interoperability└── Runtime Services lainnya
Setiap bagian memiliki tanggung jawab yang berbeda.
1. Assembly Loading
Sebelum kode dalam sebuah assembly dapat digunakan, assembly tersebut perlu dimuat ke dalam proses.
Misalnya, MyApp.dll memiliki dependency terhadap assembly lain:
Plain Text
MyApp.dll│├── LibraryA.dll├── LibraryB.dll└── LibraryC.dll
Runtime bertanggung jawab dalam proses loading assembly yang diperlukan oleh aplikasi.
Hal ini juga menjelaskan mengapa aplikasi .NET dapat menggunakan library eksternal. Ketika sebuah program menggunakan type dari library lain, runtime perlu menemukan dan memuat assembly yang menyediakan type tersebut. Jadi, MyApp.dll tidak selalu berdiri sendiri. Sebuah aplikasi biasanya terdiri dari beberapa assembly yang saling bergantung.
2. Type System
.NET memiliki sistem tipe yang disebut Common Type System (CTS).
CTS mendefinisikan bagaimana type direpresentasikan dan bagaimana type tersebut berperilaku di dalam environment .NET.
Sebagai contoh:
C#
int number = 10;string name = "Heykal";
int dan string bukan sekadar konsep yang hanya dipahami oleh compiler C#. Keduanya merupakan bagian dari type system .NET.
Hal ini menjadi penting karena .NET tidak hanya mendukung C#.
.NET juga mendukung bahasa lain seperti:
Plain Text
C#VB.NETF#
Bahasa-bahasa tersebut dapat menghasilkan IL dan metadata yang mengikuti aturan type system .NET. Dengan demikian, runtime tidak perlu dibuat khusus hanya untuk C#. Runtime bekerja terhadap konsep yang lebih umum, yaitu IL, metadata, dan type system .NET.
3. JIT Compilation
CPU tidak menjalankan IL secara langsung. IL perlu diterjemahkan menjadi machine code yang sesuai dengan arsitektur CPU. Pekerjaan tersebut dapat dilakukan oleh JIT (Just-In-Time) Compiler.
Secara sederhana:
Plain Text
IL↓JIT Compiler↓Machine Code↓CPU
Misalnya terdapat method:
C#
int Add(int a, int b){return a + b;}
Compiler C# menghasilkan IL untuk method tersebut. Ketika method perlu dijalankan, runtime dapat menggunakan JIT untuk menghasilkan machine code yang sesuai dengan environment tempat aplikasi berjalan.
Karena proses compilation menuju machine code dilakukan saat runtime, mekanisme ini disebut Just-In-Time compilation. Pembahasan mengenai JIT akan menjadi bagian penting tersendiri karena JIT memiliki banyak optimasi dan tidak selalu sesederhana "seluruh DLL dikompilasi menjadi machine code sekaligus".
4. Memory Management
Program membutuhkan memory untuk menyimpan berbagai data selama execution.
Misalnya:
C#
var user = new User();var products = new List<Product>();
Pembuatan object seperti new User() membutuhkan memory.
Runtime menyediakan mekanisme untuk mengelola memory tersebut sehingga programmer tidak harus secara manual mengalokasikan dan membebaskan seluruh memory seperti pada beberapa bahasa pemrograman lain. Salah satu bagian terpenting dari mekanisme tersebut adalah Garbage Collector.
5. Garbage Collection
Dalam aplikasi .NET, object yang sudah tidak lagi digunakan pada umumnya dapat dibersihkan secara otomatis oleh Garbage Collector (GC).
Contohnya:
C#
void Process(){var data = new LargeObject();}
Setelah object tidak lagi memiliki reference yang dapat digunakan, object tersebut pada akhirnya dapat dianggap sebagai garbage.
Garbage Collector bertugas menemukan object yang sudah tidak dapat digunakan dan mengambil kembali memory yang ditempatinya.
Secara sederhana:
Plain Text
new Object()↓Object berada di memory↓Tidak lagi digunakan↓Garbage↓Garbage Collector↓Memory dapat digunakan kembali
GC merupakan salah satu alasan penting mengapa aplikasi .NET tidak biasanya mengharuskan programmer memanggil operasi seperti free() untuk setiap object yang dibuat.
Namun, Garbage Collection bukan berarti memory management menjadi tidak penting. Programmer tetap perlu memahami lifetime object, allocation, reference, dan pola penggunaan memory karena allocation yang berlebihan tetap dapat menyebabkan masalah performa.
6. Exception Handling
Runtime juga menyediakan mekanisme untuk menangani exception.
Misalnya:
C#
try{var result = 10 / 0;}catch (Exception ex){Console.WriteLine(ex.Message);}
Ketika terjadi exception, runtime terlibat dalam proses pencarian handler yang sesuai.
Runtime harus mengetahui:
-
exception apa yang terjadi,
-
stack frame mana yang sedang aktif,
-
apakah terdapat
catchyang cocok, -
bagaimana melakukan unwinding terhadap call stack,
-
dan bagaimana melanjutkan atau menghentikan execution.
Dengan demikian, try, catch, dan finally bukan hanya fitur syntax C#. Semuanya berhubungan dengan mekanisme exception handling yang disediakan oleh runtime.
7. Threading
Aplikasi modern sering menjalankan banyak pekerjaan secara bersamaan atau menggunakan asynchronous programming.
Contohnya:
C#
await GetDataAsync();
atau:
C#
Task.Run(() => ProcessData());
.NET menyediakan berbagai abstraction untuk concurrency dan asynchronous execution, seperti:
Plain Text
ThreadTaskThreadPoolSynchronization primitivesasync / await
Runtime dan library .NET bekerja bersama untuk menyediakan mekanisme tersebut.
Karena itu, ketika aplikasi menjalankan banyak pekerjaan secara concurrent, terdapat runtime services yang membantu mengelola thread dan execution context yang diperlukan.
8. Interoperability
Aplikasi .NET terkadang perlu berkomunikasi dengan kode atau sistem di luar managed environment .NET.
Misalnya:
Plain Text
.NET Application↓Native Library↓Operating System
.NET menyediakan mekanisme interoperability agar managed code dapat berinteraksi dengan native code dan resource tertentu dari operating system. Salah satu contohnya adalah P/Invoke, yang memungkinkan kode .NET memanggil fungsi dari native library. Hal ini penting karena tidak semua kemampuan sistem operasi tersedia sebagai managed API murni di dalam .NET.
Runtime Bukan Sekadar JIT
Sering muncul pemahaman bahwa:
".NET Runtime adalah JIT."
Pemahaman tersebut kurang tepat.
JIT memang merupakan salah satu komponen penting dalam runtime, tetapi runtime memiliki tanggung jawab yang jauh lebih luas.
Secara konseptual:
Plain Text
.NET Runtime│├── Assembly Loading├── Type System Support├── JIT├── Garbage Collection├── Exception Handling├── Threading├── Interoperability└── Runtime Infrastructure
JIT hanya menangani salah satu bagian dari keseluruhan proses, yaitu menghasilkan machine code dari IL. Runtime secara keseluruhan menyediakan environment tempat managed code dapat berjalan.
Lalu Apa yang Menjadi "Jantung" dari Runtime?
Semua layanan tersebut tidak berdiri sendiri-sendiri. Di dalam implementasi .NET terdapat komponen runtime yang menangani berbagai tanggung jawab inti tersebut. Dalam konteks .NET modern, implementasi runtime yang paling umum adalah CoreCLR.
CoreCLR merupakan runtime yang digunakan oleh .NET untuk menjalankan managed code dan menyediakan berbagai kemampuan penting seperti:
Plain Text
CoreCLR│├── Execution Engine├── Garbage Collector├── JIT Compiler├── Exception Handling├── Threading Support├── Type System Support└── Runtime Services
Istilah CLR (Common Language Runtime) sering digunakan sebagai istilah konseptual untuk runtime .NET, sedangkan CoreCLR merupakan salah satu implementasi runtime yang digunakan oleh .NET modern. Perbedaan istilah ini penting agar tidak tercampur antara konsep dan implementasi.
Secara sederhana:
Plain Text
CLR││ Konsep runtime .NET│└── CoreCLR│├── JIT├── GC├── Execution Engine└── Runtime Services
Managed Code dan Unmanaged Code
Keberadaan .NET Runtime juga menjelaskan istilah managed code. Kode yang berjalan di bawah pengelolaan .NET Runtime sering disebut managed code.
Contohnya:
C#
var customer = new Customer();
Runtime ikut mengelola berbagai aspek execution kode tersebut, termasuk memory management, type information, exception handling, dan layanan runtime lainnya.
Sebaliknya, native code yang berjalan secara langsung menggunakan fasilitas operating system dan CPU biasanya disebut unmanaged code.
Gambaran sederhananya:
Plain Text
Managed Code↓.NET Runtime↓Operating System↓CPU
sedangkan:
Plain Text
Native / Unmanaged Code↓Operating System↓CPU
Istilah "managed" tidak berarti runtime mengontrol setiap instruksi CPU satu per satu. Maksudnya adalah execution environment menyediakan dan mengelola berbagai layanan untuk managed code.
Mengapa .NET Membutuhkan Runtime?
Pertanyaan berikutnya adalah: mengapa C# tidak langsung menghasilkan machine code saja? Sebenarnya compiler dapat menghasilkan native machine code. Namun, pendekatan .NET memberikan lapisan abstraction yang memiliki banyak keuntungan.
Compiler dapat menghasilkan IL yang relatif independen terhadap CPU:
Plain Text
C# Compiler↓IL↓Assembly
Kemudian runtime pada environment tertentu menangani execution:
Plain Text
Assembly↓Runtime↓JIT↓Machine Code↓CPU
Dengan model tersebut, compiler C# tidak harus menghasilkan machine code terpisah untuk setiap kombinasi environment.
Sebagai contoh, satu aplikasi dapat ditujukan untuk environment seperti:
Plain Text
Windows + x64Linux + x64Linux + ARM64macOS + ARM64
Runtime yang sesuai dengan environment tersebut dapat menangani bagian execution yang spesifik terhadap operating system dan CPU.
Runtime Tidak Sama dengan Operating System
.NET Runtime juga bukan operating system. Operating system seperti Windows atau Linux bertanggung jawab terhadap hal-hal seperti:
Plain Text
ProcessVirtual MemoryFile SystemNetworkDeviceCPU SchedulingSecurity
.NET Runtime berjalan di atas operating system dan menyediakan abstraction khusus untuk aplikasi .NET.
Gambaran sederhananya:
Plain Text
Application↓.NET Libraries↓.NET Runtime↓Operating System↓Hardware
Dalam praktiknya hubungan antara komponen-komponen tersebut lebih kompleks, tetapi model tersebut cukup untuk memahami posisi runtime.
Runtime dan .NET Libraries
Ada satu hal penting lainnya: .NET Runtime dan .NET Libraries bukan hal yang sama. Misalnya kode berikut:
C#
Console.WriteLine("Hello");
Console.WriteLine() berasal dari library .NET, bukan merupakan instruksi yang secara langsung menjadi bagian dari CLR.
Secara konseptual terdapat perbedaan:
Plain Text
.NET Runtime↓Menjalankan managed code.NET Libraries↓Menyediakan API yang digunakan aplikasi
Contoh library menyediakan berbagai API untuk:
Plain Text
CollectionsFile I/ONetworkingJSONLINQHTTPReflectionThreadingDatabase accessdan banyak lagi
Runtime menyediakan execution environment, sedangkan library menyediakan berbagai functionality yang dapat digunakan oleh aplikasi. Keduanya bekerja bersama.
Bagian 10: CLR
Apa Itu CLR?
Pada bagian sebelumnya, .NET Runtime diperkenalkan sebagai environment yang menyediakan berbagai layanan agar sebuah assembly dapat benar-benar dijalankan. Di dalam pembahasan tersebut muncul istilah CLR, singkatan dari Common Language Runtime.
CLR merupakan execution environment yang menjadi bagian penting dari ekosistem .NET. CLR bertanggung jawab menyediakan berbagai layanan yang dibutuhkan oleh managed code selama program berjalan.
Secara sederhana:
Plain Text
Assembly↓CLR↓Runtime Services↓Execution
Assembly seperti MyApp.dll berisi IL dan metadata. Assembly tersebut belum berupa instruksi native yang langsung dapat dijalankan CPU. CLR menyediakan environment yang memungkinkan isi assembly tersebut dimuat, dipahami, dan dijalankan.
Namun, CLR bukan berarti sebuah program tunggal yang melakukan seluruh pekerjaan tersebut sendirian. Di dalam runtime terdapat berbagai komponen yang bekerja bersama untuk menyediakan layanan execution.
Gambaran yang lebih lengkap:
Plain Text
.NET Runtime│CLR│┌─────────────────┼──────────────────┐↓ ↓ ↓Assembly Loading JIT GC↓ ↓ ↓Type System Native Code Memory Management│├── Exception Handling├── Threading├── Interoperability└── Runtime Services
Karena itu, CLR lebih tepat dipahami sebagai lingkungan execution dan kumpulan mekanisme inti yang memungkinkan managed code berjalan, bukan sekadar sebuah compiler atau satu proses sederhana.
Mengapa Disebut "Common Language Runtime"?
Nama Common Language Runtime terdiri dari tiga konsep penting.
Common
"Common" menunjukkan bahwa runtime dirancang untuk digunakan oleh berbagai bahasa pemrograman yang mengikuti ekosistem .NET.
Contohnya:
Plain Text
C#VB.NETF#
Ketiga bahasa tersebut memiliki syntax dan karakteristik bahasa yang berbeda, tetapi dapat dikompilasi menuju IL dan metadata yang dipahami oleh runtime .NET.
Secara sederhana:
Plain Text
C# ────────┐│VB.NET ────┼──→ IL + Metadata ──→ CLR│F# ────────┘
Inilah salah satu konsep penting dalam desain .NET.
CLR tidak perlu memahami syntax C# secara langsung ketika program sudah berada pada tahap execution. Syntax seperti:
C#
ifforclassinterfaceasyncawait
merupakan bagian dari bahasa dan compiler masing-masing.
Setelah compilation selesai, runtime bekerja terhadap hasil compilation seperti IL, metadata, type information, dan runtime structures.
Language
"Language" merujuk pada berbagai bahasa pemrograman yang dapat berjalan di atas .NET. Misalnya, C# bukan satu-satunya bahasa yang dapat menghasilkan assembly .NET. Selama sebuah bahasa memiliki compiler yang dapat menghasilkan output yang sesuai dengan aturan .NET, bahasa tersebut dapat memanfaatkan runtime yang sama. Hal ini memungkinkan adanya konsep language interoperability.
Sebagai contoh konseptual:
Plain Text
C# Code↓C# Compiler↓IL↓CLR
dan:
Plain Text
F# Code↓F# Compiler↓IL↓CLR
Runtime yang digunakan pada tahap execution tetap dapat sama.
Runtime
"Runtime" berarti environment yang bekerja ketika program sedang dijalankan. Ini berbeda dengan compile time.
Pada compile time:
Plain Text
Source Code↓Compiler↓Assembly
Pada runtime:
Plain Text
Assembly↓Runtime↓Execution
Jadi, CLR berada pada tahap runtime, bukan tahap compilation.
CLR Bukan Compiler
Sampai titik ini terdapat tiga istilah yang sering muncul secara berdekatan:
Plain Text
CompilerCLRJIT
Ketiganya memang berhubungan dengan execution program .NET, tetapi memiliki tanggung jawab yang berbeda.
Secara sederhana:
| Komponen | Tanggung jawab utama |
|---|---|
| Compiler | Mengubah source code menjadi IL dan metadata |
| CLR | Menyediakan environment untuk menjalankan managed code |
| JIT | Mengubah IL menjadi native machine code |
Jika digambarkan sebagai alur:
Plain Text
Source Code│↓Compiler│↓IL + Metadata│↓CLR│↓JIT│↓Native Machine Code│↓CPU
Pembagian tanggung jawab tersebut sangat penting. Compiler tidak bertugas menjalankan program. CLR bukan compiler source code. JIT bukan compiler C#. Ketiganya berada pada tahap yang berbeda.
Apa yang Dilakukan Compiler?
Compiler bekerja ketika source code sedang dikompilasi.
Misalnya terdapat kode:
C#
int Add(int a, int b){return a + b;}
Compiler C# membaca syntax tersebut dan melakukan berbagai pekerjaan seperti:
Plain Text
Source Code↓Lexing / Parsing↓Semantic Analysis↓Type Checking↓IL Generation↓Metadata Generation↓Assembly
Hasil akhirnya adalah assembly yang berisi IL dan metadata.
Secara sederhana:
Plain Text
Add()↓C# Compiler↓IL
Compiler tidak perlu mengetahui CPU tempat aplikasi nantinya dijalankan untuk menghasilkan IL dalam model compilation normal .NET.
Apa yang Dilakukan CLR?
Setelah assembly tersedia, CLR menyediakan environment agar assembly tersebut dapat dijalankan.
Tugas CLR mencakup berbagai hal, seperti:
Plain Text
Assembly LoadingType SystemJIT CompilationGarbage CollectionException HandlingThreadingInteropRuntime Services
Dengan kata lain, CLR menyediakan banyak fasilitas yang dibutuhkan managed application selama execution.
Misalnya sebuah aplikasi membuat object:
C#
var customer = new Customer();
Ada banyak hal yang harus terjadi di balik satu baris sederhana tersebut.
Runtime perlu mengetahui type Customer, mengalokasikan memory yang diperlukan, membuat object, mengatur metadata yang terkait dengan type tersebut, dan memastikan object dapat digunakan oleh managed code.
Programmer cukup melihat:
C#
var customer = new Customer();
Tetapi execution environment melakukan lebih banyak pekerjaan di belakang layar.
Apa yang Dilakukan JIT?
JIT memiliki tugas yang lebih spesifik. IL bukan native machine code. CPU tidak secara langsung memahami IL. JIT mengambil IL dan menghasilkan native code yang dapat dijalankan oleh CPU.
Secara sederhana:
Plain Text
IL↓JIT Compiler↓Native Machine Code↓CPU
Misalnya CPU yang digunakan adalah x64. JIT dapat menghasilkan instruksi machine code yang sesuai dengan arsitektur tersebut.
Jika environment menggunakan ARM64, JIT dapat menghasilkan machine code yang sesuai dengan ARM64.
Dengan demikian, terdapat pemisahan tanggung jawab:
Plain Text
Compiler↓Menghasilkan IL yang relatif portableJIT↓Menghasilkan native code sesuai environmentCPU↓Menjalankan native code
Detail mengenai proses tersebut akan dibahas pada bagian khusus tentang JIT.
CLR Tidak Berarti "IL Langsung Dijalankan"
Salah satu miskonsepsi yang sering muncul adalah:
CLR menjalankan IL secara langsung.
Kalimat tersebut terlalu sederhana dan dapat menyebabkan kesalahpahaman.
CLR menyediakan environment execution, tetapi CPU tetap membutuhkan instruksi yang sesuai dengan arsitekturnya.
Dalam execution model yang melibatkan JIT, salah satu jalur utamanya adalah:
Plain Text
Assembly↓CLR↓IL↓JIT↓Native Code↓CPU
Jadi, CLR tidak membuat CPU tiba-tiba memahami IL.
CLR mengatur berbagai mekanisme yang memungkinkan IL pada akhirnya diterjemahkan dan dieksekusi.
CLR dan Managed Code
CLR juga berkaitan erat dengan istilah managed code. Managed code adalah kode yang execution-nya berada di bawah pengelolaan .NET runtime.
Contohnya:
C#
class Customer{public string Name { get; set; }}
Ketika object dibuat:
C#
var customer = new Customer();
object tersebut berada dalam memory yang dikelola oleh runtime.
Runtime dapat menyediakan layanan seperti:
Plain Text
Memory ManagementGarbage CollectionType SafetyException HandlingThreading
Karena itulah istilah "managed" digunakan.
Sebaliknya, native code yang tidak berada di bawah pengelolaan runtime .NET disebut unmanaged code.
Namun, managed code tetap dapat berinteraksi dengan unmanaged code melalui mekanisme interoperability.
Contohnya:
Plain Text
Managed Code↓.NET Runtime↓Interop↓Native Code
Hal ini memungkinkan aplikasi .NET berkomunikasi dengan native library atau API operating system tertentu.
CLR dan Type System
CLR juga memiliki hubungan erat dengan type system .NET.
Misalnya:
C#
int age = 26;string name = "Heykal";
Type seperti int dan string memiliki representasi dan aturan yang dipahami oleh environment .NET.
Hal ini menjadi penting karena bahasa-bahasa .NET yang berbeda harus memiliki pemahaman yang konsisten terhadap type.
Misalnya:
Plain Text
C#│├── int├── string└── class
dan:
Plain Text
F#│├── int├── string└── type
Compiler dari masing-masing bahasa dapat menghasilkan IL dan metadata yang mengikuti aturan type system .NET. CLR kemudian dapat menggunakan informasi tersebut ketika program dijalankan.
CLR dan Garbage Collector
CLR juga berhubungan dengan Garbage Collector (GC).
Ketika program membuat object:
C#
var customer = new Customer();
memory diperlukan untuk menyimpan object tersebut.
Runtime menyediakan mekanisme memory management dan GC untuk mengelola object-object managed tersebut.
Secara konseptual:
Plain Text
Application↓Create Object↓Managed Memory↓Object Tidak Lagi Digunakan↓Garbage Collector↓Memory Dapat Digunakan Kembali
GC bukan fitur yang berada di luar runtime. Garbage Collection merupakan salah satu layanan inti yang mendukung managed execution.
CLR dan Exception
Ketika terjadi exception:
C#
throw new InvalidOperationException();
runtime perlu menangani exception tersebut.
Misalnya terdapat:
C#
try{Process();}catch (Exception ex){Log(ex);}
Runtime harus mencari handler yang sesuai dengan exception yang terjadi dan mengatur proses unwinding terhadap call stack.
Secara konseptual:
Plain Text
Method A↓Method B↓Method C↓Exception↓Stack Unwinding↓Catch Handler
Proses tersebut merupakan bagian dari runtime execution.
CLR Bukan Satu File DLL Saja
Istilah CLR terkadang dibayangkan sebagai satu file atau satu executable tertentu. Dalam kenyataannya, runtime .NET terdiri dari berbagai komponen yang bekerja bersama. Pada .NET modern, salah satu implementasi runtime utamanya adalah CoreCLR.
CoreCLR menyediakan berbagai komponen inti seperti:
Plain Text
CoreCLR│├── Execution Engine├── Garbage Collector├── JIT Compiler├── Exception Handling├── Threading├── Type System└── Runtime Infrastructure
Karena itu, ketika membahas CLR, penting untuk membedakan antara konsep CLR dan implementasi runtime tertentu.
Secara historis, istilah CLR sangat erat dengan .NET Framework. Pada .NET modern, runtime yang digunakan adalah CoreCLR sebagai salah satu implementasi utama dari execution environment .NET.
CLR, CoreCLR, dan .NET Runtime
Ketiga istilah berikut sering digunakan dalam pembahasan .NET:
Plain Text
CLRCoreCLR.NET Runtime
Ketiganya berhubungan, tetapi tidak selalu memiliki arti yang persis sama.
Secara sederhana:
.NET Runtime
Istilah umum untuk environment yang memungkinkan aplikasi .NET berjalan.
CLR
Istilah konseptual dan historis untuk Common Language Runtime, yaitu runtime environment yang menyediakan layanan execution untuk managed code.
CoreCLR
Implementasi runtime modern yang menjadi bagian inti dari .NET dan menyediakan execution engine, GC, JIT, serta berbagai runtime services.
Gambaran sederhananya:
Plain Text
.NET│├── Runtime│ ││ └── CoreCLR│ ├── Execution Engine│ ├── JIT│ ├── GC│ └── Runtime Services│└── Libraries├── System.*├── Collections├── Networking└── ...
Diagram tersebut merupakan penyederhanaan konseptual. Struktur internal .NET sebenarnya lebih kompleks, tetapi cukup untuk memahami hubungan antaristilah.
Mengapa CLR Penting?
Tanpa runtime, assembly yang berisi IL dan metadata belum memiliki environment untuk melakukan managed execution. CLR menjadi lapisan yang menghubungkan assembly dengan execution environment.
Plain Text
Assembly││ IL + Metadata↓CLR / Runtime│├── Load Assembly├── Understand Types├── Manage Memory├── Handle Exceptions├── Manage Execution└── Invoke JIT│↓Native Code↓CPU
Dari sudut pandang ini, CLR dapat dipandang sebagai jembatan antara dunia assembly .NET dan dunia execution. Assembly menjelaskan apa yang harus dijalankan. Runtime menyediakan environment untuk menjalankannya. JIT membantu menghasilkan native code. CPU akhirnya menjalankan instruksi tersebut.
Bagian 11: JIT Compiler
IL Masih Belum Bisa Dijalankan CPU
Jika seluruh perjalanan program .NET dari bagian-bagian sebelumnya dirangkai kembali, alurnya menjadi:
Plain Text
C#↓Compiler↓IL + Metadata↓Assembly↓.NET Runtime / CLR
Pada titik ini, program memang sudah berada di dalam runtime, tetapi masih terdapat satu masalah penting: CPU belum menerima native machine instructions.
Assembly seperti:
Plain Text
MyApp.dll
masih berisi IL (Intermediate Language), bukan instruksi machine code yang secara langsung dipahami oleh CPU.
CPU memiliki instruction set sendiri. Sebagai contoh, CPU x64 memiliki instruksi machine code untuk arsitektur x86-64, sedangkan ARM64 memiliki instruction set yang berbeda. IL dirancang sebagai instruction set untuk environment .NET, bukan sebagai instruction set untuk CPU tertentu.
Karena itu, masih diperlukan sebuah proses untuk mengubah:
Plain Text
IL↓Native Machine Code
Komponen yang bertanggung jawab atas proses tersebut adalah JIT Compiler, atau Just-In-Time Compiler.
Secara sederhana:
Plain Text
IL↓JIT Compiler↓Native Machine Code↓CPU
Inilah bagian yang menjawab pertanyaan yang sebelumnya masih menggantung:
Bagaimana IL yang terdapat di dalam assembly akhirnya bisa dijalankan oleh CPU?
Jawabannya: IL perlu dikompilasi menjadi native code, dan salah satu mekanisme utama untuk melakukan hal tersebut adalah JIT.
Apa Itu JIT Compiler?
JIT adalah singkatan dari Just-In-Time Compiler. JIT merupakan compiler yang bekerja pada saat runtime untuk menghasilkan native machine code dari IL. Perbedaannya dengan compiler C# dapat dilihat dari input dan output-nya.
C# Compiler:
Plain Text
C# Source Code↓C# Compiler↓IL + Metadata↓Assembly
Sedangkan JIT:
Plain Text
IL↓JIT Compiler↓Native Machine Code
Dengan demikian, terdapat dua tahap compilation yang penting dalam model execution .NET:
Plain Text
Compile TimeC# ─────────────────────→ IL││↓Runtime│↓IL ─────────────────────→ Native Code
Tahap pertama dilakukan oleh compiler bahasa seperti C# compiler. Tahap kedua dilakukan oleh JIT pada runtime. Karena itu, istilah "compiled language" dan "JIT" tidak saling bertentangan. Aplikasi .NET dapat mengalami compilation pada lebih dari satu tahap.
Mengapa Disebut "Just-In-Time"?
Nama Just-In-Time menggambarkan kapan compilation menuju native code dilakukan. Compilation tersebut tidak harus dilakukan terhadap seluruh aplikasi ketika aplikasi pertama kali dibangun. Sebaliknya, runtime dapat melakukan compilation ketika method diperlukan untuk execution.
Model sederhananya:
Plain Text
Application Starts↓Runtime Loads Assembly↓Method Dipanggil↓JIT↓Native Code↓CPU
Misalnya terdapat method:
C#
int Add(int a, int b){return a + b;}
Ketika program pertama kali membutuhkan method tersebut, runtime dapat melakukan proses yang secara konseptual seperti:
Plain Text
Add()↓IL↓JIT↓Native Code↓CPU
Setelah native code tersedia, execution dapat menggunakan hasil compilation tersebut. Hal ini membuat JIT berbeda dari pendekatan yang melakukan seluruh native compilation sebelum program dijalankan.
JIT Tidak Mengompilasi Seluruh DLL Sekaligus
Salah satu kesalahpahaman yang sering muncul adalah bahwa ketika aplikasi dijalankan:
Plain Text
MyApp.dll↓JIT↓Seluruh DLL menjadi native code
Model tersebut terlalu sederhana dan tidak menggambarkan cara kerja JIT modern.
Pada dasarnya, JIT bekerja pada level method.
Misalnya sebuah assembly memiliki:
C#
void MethodA() { }void MethodB() { }void MethodC() { }void MethodD() { }
Tidak berarti seluruh method tersebut harus langsung dikompilasi menjadi native code ketika aplikasi mulai berjalan.
Secara konseptual:
Plain Text
MyApp.dll│├── MethodA├── MethodB├── MethodC└── MethodD
Ketika hanya MethodA() yang diperlukan:
Plain Text
MethodA↓JIT↓Native MethodA
Method lain mungkin belum perlu dikompilasi pada saat tersebut. Hal ini merupakan salah satu alasan istilah Just-In-Time digunakan. Runtime dapat menunda pekerjaan compilation sampai memang diperlukan.
Contoh Sederhana
Misalnya terdapat program:
C#
class Calculator{public int Add(int a, int b){return a + b;}public int Multiply(int a, int b){return a * b;}}
Kemudian program hanya memanggil:
C#
var calculator = new Calculator();var result = calculator.Add(10, 20);
Secara konseptual, ketika Add() perlu dijalankan:
Plain Text
calculator.Add()↓IL Method Add↓JIT↓Native Code↓CPU
Tidak ada alasan sederhana untuk menganggap Multiply() harus langsung dikompilasi hanya karena method tersebut terdapat dalam assembly yang sama.
Jika Multiply() tidak pernah digunakan, compilation native code untuk method tersebut mungkin tidak diperlukan dalam execution path tersebut.
Detail aktual mengenai kapan dan bagaimana method dikompilasi bergantung pada runtime, mode compilation, optimasi, dan berbagai kondisi lainnya. Namun prinsip pentingnya tetap sama:
JIT bekerja berdasarkan kebutuhan execution, bukan sekadar mengubah seluruh assembly menjadi native code sekaligus.
JIT Bekerja terhadap IL
Untuk memahami JIT dengan benar, penting untuk melihat apa yang sebenarnya menjadi input JIT. C# Compiler sebelumnya sudah menghasilkan IL.
Misalnya source code:
C#
int Add(int a, int b){return a + b;}
secara konseptual menjadi:
Plain Text
C# Source↓C# Compiler↓IL
IL tersebut kemudian menjadi bagian dari assembly:
Plain Text
MyApp.dll│├── IL├── Metadata├── Type Information└── References
Ketika method diperlukan, runtime menemukan IL untuk method tersebut.
Kemudian:
Plain Text
IL Method↓JIT Compiler↓Native Code
Jadi JIT tidak membaca source code C#.
JIT juga bukan compiler yang memahami syntax C# seperti:
C#
ifforclassinterfaceasyncawait
Syntax tersebut sudah diproses oleh compiler bahasa sebelumnya.
Input JIT adalah representation yang lebih rendah, yaitu IL beserta informasi yang dibutuhkan runtime.
Native Code Itu Apa?
Istilah native code berarti machine code yang ditujukan untuk arsitektur CPU tertentu.
Sebagai contoh, terdapat perbedaan antara:
Plain Text
x64ARM64
Keduanya memiliki instruction set yang berbeda.
Karena itu, hasil JIT dapat berbeda tergantung environment.
Secara konseptual:
Plain Text
IL│┌──────┴──────┐↓ ↓JIT untuk x64 JIT untuk ARM64↓ ↓Native x64 Native ARM64↓ ↓x64 CPU ARM64 CPU
Hal ini memberikan fleksibilitas penting.
Assembly dapat berisi IL yang relatif tidak bergantung pada instruction set CPU tertentu, sementara JIT dapat menghasilkan native code yang sesuai dengan CPU tempat aplikasi sedang dijalankan.
Mengapa Tidak Langsung Menghasilkan Native Code dari C#?
Pertanyaan berikutnya adalah:
Jika tujuan akhirnya adalah machine code, mengapa C# Compiler tidak langsung menghasilkan native code?
Pendekatan tersebut sebenarnya memungkinkan dan memang ada dalam teknologi lain.
Namun model .NET memiliki keuntungan karena menggunakan intermediate representation seperti IL.
Dengan model tersebut:
Plain Text
C#↓IL↓JIT↓Native Code
compiler bahasa dan target CPU dapat lebih terpisah.
Compiler C# bertanggung jawab terhadap bahasa C#:
Plain Text
C#↓IL
Sedangkan JIT menangani translation menuju CPU:
Plain Text
IL↓Native Code
Pemisahan ini memungkinkan satu bahasa menghasilkan IL yang kemudian dapat dieksekusi pada berbagai arsitektur melalui runtime yang sesuai.
IL Berfungsi sebagai Lapisan Perantara
IL dapat dipahami sebagai lapisan perantara antara bahasa pemrograman dan CPU. Tanpa lapisan tersebut, compiler bahasa harus menangani banyak target CPU secara langsung.
Dengan IL:
Plain Text
C#↓Compiler↓IL↓┌───────┼────────┐↓ ↓ ↓x64 ARM64 architecture lain↓ ↓ ↓Native Native Native↓ ↓ ↓CPU CPU CPU
Bahasa lain juga dapat menggunakan jalur yang sama:
Plain Text
C# ────────┐│VB.NET ────┼──→ IL ──→ JIT ──→ Native Code│F# ────────┘
Inilah salah satu konsep fundamental yang memungkinkan ekosistem bahasa .NET memiliki runtime bersama.
Apakah JIT Selalu Berjalan Saat Method Dipanggil?
Tidak tepat jika JIT dipahami sebagai:
Plain Text
Setiap kali method dipanggil↓JIT ulang↓Native Code
Jika sebuah method sudah berhasil dikompilasi menjadi native code, runtime tidak perlu selalu mengompilasi method tersebut dari awal setiap kali method dipanggil.
Secara konseptual:
Plain Text
First Call↓IL↓JIT↓Native Code↓ExecuteSubsequent Calls↓Existing Native Code↓Execute
Dengan kata lain, hasil compilation dapat digunakan kembali selama masih relevan untuk execution environment tersebut. Jika tidak demikian, biaya JIT akan sangat besar karena setiap pemanggilan method harus melakukan compilation ulang.
JIT Juga Melakukan Optimasi
JIT bukan sekadar translator sederhana. Selain menerjemahkan IL menjadi native code, JIT juga dapat melakukan berbagai optimasi.
Misalnya secara konseptual:
C#
int Add(int a, int b){return a + b;}
JIT dapat menghasilkan native code yang efisien untuk operasi tersebut.
Dalam kasus yang lebih kompleks, compiler JIT dapat melakukan berbagai optimasi seperti:
Plain Text
InliningConstant propagationDead code eliminationDevirtualizationBounds-check optimizationsRegister allocationdan optimasi lainnya
Tujuan akhirnya adalah menghasilkan native code yang dapat dieksekusi secara efisien oleh CPU.
Namun optimasi tersebut bukan berarti semua kode selalu menjadi lebih cepat secara otomatis. JIT harus mempertimbangkan berbagai trade-off antara waktu compilation, penggunaan memory, ukuran native code, dan performa execution.
Inlining
Salah satu optimasi penting yang dapat dilakukan JIT adalah method inlining.
Misalnya:
C#
int Add(int a, int b){return a + b;}int Calculate(){return Add(10, 20);}
Secara konsep, tanpa inlining terdapat pemanggilan:
Plain Text
Calculate()↓Add()↓return
Dengan inlining, JIT dapat mengoptimalkan execution sehingga isi method kecil seperti Add() secara konseptual dapat ditempatkan langsung pada caller:
Plain Text
Calculate()↓10 + 20
Ini bukan berarti source code C# benar-benar diubah menjadi seperti itu. Inlining adalah optimasi pada representation yang digunakan untuk menghasilkan native code.
Inlining dapat menghilangkan overhead pemanggilan method dan juga membuka peluang optimasi lainnya.
JIT Memiliki Trade-Off
JIT memberikan fleksibilitas dan optimasi, tetapi proses compilation juga membutuhkan waktu dan resource.
Misalnya:
Plain Text
Application Start↓Method pertama kali digunakan↓JIT Compilation↓Native Code↓Execution
JIT compilation membutuhkan CPU time.
Karena itu, jika terlalu banyak code harus dikompilasi saat startup atau ketika execution pertama kali terjadi, startup time dapat meningkat. Sebaliknya, jika terlalu sedikit optimasi dilakukan, execution code dapat kurang optimal.
Runtime harus mencari keseimbangan antara:
Plain Text
Compilation Cost↕Execution Performance
Inilah salah satu alasan runtime modern memiliki berbagai strategi compilation dan optimasi.
JIT dan Startup Performance
Bayangkan sebuah aplikasi memiliki banyak method:
Plain Text
Method AMethod BMethod C...Method Z
Jika seluruh method langsung dikompilasi sebelum application benar-benar melakukan pekerjaan, startup dapat menjadi lebih mahal.
JIT memungkinkan runtime menunda sebagian compilation:
Plain Text
Application Starts↓Load Required Components↓Run Application↓Compile Methods When Needed
Pendekatan seperti ini dapat membantu menghindari compilation terhadap code yang ternyata tidak pernah digunakan. Namun, JIT juga dapat menimbulkan biaya pada execution pertama sebuah method. Hal tersebut sering disebut sebagai first-call compilation overhead.
Tiered Compilation
Pada .NET modern, model JIT juga lebih kompleks daripada sekadar:
Plain Text
IL↓JIT↓Native Code
Salah satu mekanisme penting adalah tiered compilation.
Secara konseptual, sebuah method dapat pertama-tama dikompilasi dengan cepat menggunakan optimasi yang lebih ringan. Kemudian, jika method tersebut ternyata sering digunakan, runtime dapat melakukan compilation ulang dengan optimasi yang lebih agresif.
Gambaran sederhananya:
Plain Text
Method↓Initial JIT↓Fast Native Code↓Method sering digunakan↓Optimized Compilation↓More Optimized Native Code
Pendekatan tersebut memungkinkan runtime menyeimbangkan dua kebutuhan:
Plain Text
Fast startup+Good long-running performance
Artinya, runtime tidak harus memilih antara "startup cepat" dan "native code yang sangat optimal" secara mutlak. Compilation dapat dilakukan secara bertahap berdasarkan informasi yang diperoleh selama program berjalan.
JIT Dapat Mengetahui Informasi Saat Runtime
Salah satu keuntungan JIT dibandingkan compilation yang seluruhnya dilakukan sebelum program dijalankan adalah JIT dapat melihat kondisi execution environment yang sebenarnya.
Pada saat runtime, informasi seperti berikut tersedia:
Plain Text
CPU architectureRuntime environmentMethod usageType informationExecution patterns
Informasi tersebut dapat membantu runtime melakukan optimasi yang lebih sesuai dengan kondisi actual execution. Misalnya, sebuah method ternyata sangat sering dipanggil. Runtime dapat menggunakan informasi tersebut untuk menentukan bahwa method tersebut layak mendapatkan optimasi lebih agresif. Dengan demikian, runtime tidak hanya bergantung pada informasi yang tersedia ketika source code pertama kali dikompilasi.
JIT Bukan Satu-Satunya Cara .NET Menghasilkan Native Code
Penting untuk tidak menyederhanakan model .NET menjadi:
Semua aplikasi .NET selalu dijalankan menggunakan JIT.
Dalam .NET modern terdapat beberapa strategi deployment dan compilation.
Salah satunya adalah Ahead-of-Time (AOT) compilation, yang melakukan compilation menuju native code sebelum runtime execution.
Secara sederhana:
JIT
Plain Text
IL↓Runtime↓JIT↓Native Code↓CPU
AOT
Plain Text
IL / Intermediate Representation↓AOT Compilation↓Native Code↓Application↓CPU
AOT dapat mengurangi kebutuhan compilation saat runtime dan dapat memberikan keuntungan tertentu pada startup time, deployment, atau ukuran aplikasi, tergantung skenario.
Karena itu, model:
Plain Text
IL → JIT → CPU
merupakan model fundamental untuk memahami JIT, tetapi bukan satu-satunya model execution yang tersedia dalam ekosistem .NET modern.
Bagian 12: AOT (Ahead-of-Time Compilation)
Pada bagian sebelumnya, pembahasan berfokus pada JIT (Just-In-Time Compiler), yaitu mekanisme yang menerjemahkan IL menjadi native code ketika aplikasi berjalan.
Model sederhananya adalah:
Plain Text
C#↓Compiler↓IL↓Assembly↓.NET Runtime↓JIT↓Native Code↓CPU
Model tersebut memiliki keuntungan karena compilation menuju native code dapat dilakukan berdasarkan environment tempat aplikasi benar-benar berjalan. Namun, ada konsekuensi: sebagian pekerjaan compilation harus dilakukan saat runtime.
Pertanyaan berikutnya menjadi menarik:
Bagaimana jika native code sudah dibuat sebelum aplikasi dijalankan?
Di sinilah AOT, atau Ahead-of-Time Compilation, masuk.
Apa Itu AOT?
AOT adalah singkatan dari Ahead-of-Time Compilation. Secara sederhana, AOT berarti melakukan compilation menuju native code sebelum aplikasi dijalankan.
Jika JIT melakukan:
Plain Text
Runtime↓IL↓JIT↓Native Code
AOT melakukan sebagian besar proses tersebut lebih awal:
Plain Text
Build / Publish↓AOT Compiler↓Native Code↓Application↓CPU
Kata Ahead-of-Time berarti compilation dilakukan "sebelum waktunya execution".
Perbandingan sederhananya:
Plain Text
JIT:Application Starts↓IL↓JIT↓Native Code↓CPU
sedangkan:
Plain Text
AOT:Build / Publish↓IL↓AOT↓Native Code↓Application Starts↓CPU
Perbedaan utama terletak pada kapan compilation menuju native code dilakukan.
JIT vs AOT
Perbedaan paling mudah dipahami adalah sebagai berikut:
| JIT | AOT |
|---|---|
| Compilation dilakukan saat runtime | Compilation dilakukan sebelum runtime |
| Membutuhkan compilation saat aplikasi berjalan | Sebagian besar compilation sudah dilakukan sebelumnya |
| Dapat menggunakan informasi actual runtime | Lebih banyak keputusan dibuat sebelum aplikasi berjalan |
| Dapat menimbulkan biaya JIT saat execution | Dapat mengurangi biaya compilation saat startup |
| Runtime melakukan lebih banyak pekerjaan | Sebagian pekerjaan dipindahkan ke build/publish |
Secara sederhana:
Plain Text
JITBuild Time Runtime│ ││ ↓│ JIT│ ↓│ Native Code│ ↓└────────────→ CPU
Sedangkan:
Plain Text
AOTBuild Time│↓AOT Compilation│↓Native Code│↓Application│↓Runtime│↓CPU
AOT memindahkan sebagian pekerjaan yang biasanya dilakukan saat runtime ke tahap build atau publish.
Mengapa AOT Dibutuhkan?
JIT memiliki keuntungan yang besar, tetapi JIT juga memiliki biaya.
Bayangkan sebuah aplikasi baru dijalankan:
Plain Text
Application Start↓Load Runtime↓Load Assembly↓Method Dipanggil↓JIT Compilation↓Native Code↓Execution
Pada execution pertama, compilation membutuhkan waktu dan resource. Untuk aplikasi tertentu, startup time sangat penting.
Contohnya:
Plain Text
Command Line ToolServerless FunctionCloud ServiceMobile ApplicationContainerSmall Utility
Pada aplikasi yang hanya berjalan sebentar, waktu yang digunakan untuk melakukan JIT bisa menjadi bagian yang cukup signifikan dari keseluruhan execution. AOT dapat mengurangi sebagian pekerjaan tersebut karena native code sudah tersedia sebelum aplikasi dimulai.
Contoh Sederhana
Misalnya terdapat method:
C#
int Calculate(int a, int b){return a + b;}
Dengan model JIT:
Plain Text
Build↓IL↓ApplicationApplication Start↓Method dipanggil↓JIT↓Native Code↓Execute
Dengan AOT:
Plain Text
Build↓IL↓AOT↓Native Code↓ApplicationApplication Start↓Native Code↓Execute
Dengan demikian, ketika application startup terjadi, sebagian compilation sudah tidak perlu dilakukan lagi.
AOT Bukan Berarti Tidak Ada Runtime
Ini adalah poin yang sangat penting.
AOT bukan berarti aplikasi tidak membutuhkan runtime sama sekali.
Kesalahpahaman yang sering muncul adalah:
Plain Text
AOT↓Tidak ada runtime
Padahal tidak selalu demikian.
Aplikasi tetap dapat membutuhkan berbagai runtime services, seperti:
Plain Text
Memory ManagementGarbage CollectionException HandlingThreadingType SystemInteropRuntime Services
Perbedaannya adalah bagian compilation menuju native code telah dilakukan sebelumnya.
Secara konseptual:
Plain Text
AOT Application↓Native Code↓.NET Runtime Services↓Operating System↓CPU
Jadi AOT tidak menghilangkan seluruh runtime. AOT terutama mengubah kapan compilation dilakukan dan seberapa banyak pekerjaan compilation yang harus dilakukan saat runtime.
AOT Tidak Sama dengan "Compile Sekali Menjadi EXE"
Ada pemahaman sederhana bahwa:
AOT berarti source code langsung menjadi satu file .exe.
Tidak sesederhana itu.
Output AOT bergantung pada teknologi AOT yang digunakan, target platform, konfigurasi deployment, dan kebutuhan runtime.
Hasil akhirnya dapat terdiri dari:
Plain Text
Native ExecutableNative LibrariesRuntime ComponentsConfigurationResourcesDependencies
Jadi AOT sebaiknya dipahami sebagai strategi compilation, bukan sekadar format file output tertentu.
AOT dan Native Code
Perbedaan fundamental antara JIT dan AOT dapat dilihat dari posisi native code.
JIT
Native code dihasilkan ketika application berjalan:
Plain Text
Assembly↓IL↓JIT↓Native Code
AOT
Native code dihasilkan sebelum application berjalan:
Plain Text
Assembly / Intermediate Representation↓AOT Compiler↓Native Code
Setelah aplikasi dijalankan, CPU dapat langsung mengeksekusi native code yang sudah tersedia.
Keuntungan AOT
AOT memiliki beberapa keuntungan yang menarik.
1. Startup Lebih Cepat
Karena sebagian compilation sudah dilakukan sebelumnya, application dapat mengurangi pekerjaan compilation ketika startup.
Model sederhananya:
Plain Text
JIT:Start↓Load↓JIT↓Execute
Sedangkan:
Plain Text
AOT:Start↓Load↓Execute
Ini tidak berarti semua aplikasi AOT pasti startup-nya lebih cepat dalam setiap kondisi. Startup juga dipengaruhi oleh ukuran application, loading dependency, initialization, operating system, storage, dan banyak faktor lain. Namun menghilangkan sebagian pekerjaan JIT dapat memberikan keuntungan yang signifikan pada skenario tertentu.
2. Mengurangi Runtime Compilation
JIT membutuhkan CPU time untuk melakukan compilation saat application berjalan.
AOT memindahkan sebagian pekerjaan tersebut ke tahap build atau publish.
Plain Text
JIT:Runtime CPU├── Application Work└── Compilation WorkAOT:Build CPU└── Compilation WorkRuntime CPU└── Application Work
Dengan demikian, sebagian biaya compilation dibayar lebih awal.
3. Startup yang Lebih Konsisten
Karena native code sudah tersedia, application tidak harus melakukan compilation method tertentu ketika pertama kali digunakan. Hal ini dapat membantu application yang sangat sensitif terhadap latency pada startup atau first request. Contohnya dapat ditemukan pada beberapa jenis serverless workload.
4. Deployment Tertentu Dapat Menjadi Lebih Ringkas
AOT dapat memungkinkan application dikirim dalam bentuk yang lebih self-contained dan lebih teroptimasi untuk target tertentu. Namun ukuran akhir deployment sangat bergantung pada konfigurasi dan jenis AOT yang digunakan. AOT bukan jaminan bahwa ukuran aplikasi selalu lebih kecil.
Kekurangan AOT
AOT juga memiliki trade-off.
1. Compilation Membutuhkan Waktu Lebih Lama
Jika JIT melakukan compilation ketika application berjalan, AOT melakukan lebih banyak pekerjaan ketika build atau publish.
Akibatnya:
Plain Text
Build / Publish↓AOT Compilation↓Native Application
dapat membutuhkan waktu dan resource lebih besar dibandingkan deployment biasa.
2. Informasi Runtime Lebih Terbatas
JIT memiliki keuntungan karena bekerja ketika application benar-benar berjalan.
Pada saat itu runtime dapat mengetahui kondisi aktual seperti:
Plain Text
CPUExecution PatternMethod UsageType InformationRuntime Environment
AOT melakukan lebih banyak keputusan sebelum application berjalan.
Karena itu, beberapa optimasi yang bergantung pada informasi runtime dapat memiliki keterbatasan dibandingkan JIT.
3. Reflection Dapat Menjadi Lebih Kompleks
Reflection memungkinkan program menemukan dan menggunakan type atau member secara dinamis.
Contohnya:
C#
var type = Type.GetType("MyApp.Customer");
Dengan JIT dan runtime compilation tradisional, runtime dapat lebih fleksibel menghadapi pola penggunaan seperti ini. Pada AOT, compiler perlu mengetahui sebanyak mungkin code yang benar-benar akan digunakan.
Jika sebuah type hanya diketahui secara dinamis melalui reflection, compiler dapat mengalami kesulitan menentukan bahwa type tersebut harus tersedia dalam native output. Karena itu, beberapa pola reflection dan dynamic code dapat membutuhkan konfigurasi atau anotasi tambahan ketika menggunakan AOT.
Trimming dan AOT
Pembahasan AOT sering berkaitan dengan konsep lain yang disebut trimming.
Bayangkan sebuah library memiliki:
Plain Text
1000 Types
tetapi application hanya menggunakan:
Plain Text
50 Types
Jika seluruh library selalu dimasukkan ke dalam deployment, banyak code yang sebenarnya tidak diperlukan. Trimming mencoba mengidentifikasi code yang tidak digunakan dan menghapusnya dari output.
Secara sederhana:
Plain Text
Library│├── Type A ← digunakan├── Type B ← digunakan├── Type C ← tidak digunakan├── Type D ← tidak digunakan└── Type E ← digunakan↓Trimming↓Hanya code yangdiperlukan
AOT dan trimming sering digunakan bersama karena keduanya sama-sama berhubungan dengan menghasilkan deployment yang lebih teroptimasi. Namun, trimming memiliki tantangan yang sama dengan dynamic behavior. Jika application menggunakan sesuatu secara dinamis, compiler mungkin tidak dapat mengetahui bahwa code tersebut sebenarnya diperlukan.
Mengapa Reflection Menjadi Tantangan?
Misalnya:
C#
string typeName = GetTypeNameFromConfiguration();var type = Type.GetType(typeName);
Compiler tidak selalu dapat mengetahui nilai typeName ketika melakukan AOT compilation.
Misalnya ternyata nilainya:
Plain Text
"MyApp.Customer"
tetapi compiler tidak mengetahui hal tersebut pada compile time.
Maka muncul masalah:
Plain Text
Compile Time↓"Type apa yang sebenarnya akan digunakan?"↓Tidak diketahui secara pasti
JIT memiliki fleksibilitas lebih besar karena runtime dapat mengetahui informasi tersebut ketika program benar-benar berjalan. AOT membutuhkan lebih banyak informasi pada saat compilation.
Karena itu, framework dan library modern yang mendukung AOT biasanya harus memperhatikan:
Plain Text
ReflectionDynamic CodeRuntime Type DiscoverySerializationDependency Injection
dan pola-pola lain yang sulit dianalisis secara statis.
AOT dan Dependency Injection
Dependency Injection merupakan contoh menarik.
Kode seperti:
C#
services.AddScoped<IUserService, UserService>();
cukup mudah dianalisis karena hubungan antara interface dan implementation ditulis secara eksplisit.
Namun jika sebuah framework melakukan discovery secara dinamis:
Plain Text
Scan Assembly↓Find Types↓Find Interfaces↓Register Automatically
AOT perlu mengetahui hasil discovery tersebut pada saat compilation atau melalui metadata/configuration yang sesuai. Inilah salah satu alasan mengapa beberapa library yang sangat bergantung pada reflection perlu penyesuaian agar kompatibel dengan AOT.
AOT di .NET Modern
Dalam ekosistem .NET modern terdapat beberapa bentuk AOT. Salah satu yang penting adalah Native AOT. Native AOT memungkinkan aplikasi .NET dikompilasi menjadi native executable sebelum dijalankan.
Model konseptualnya:
Plain Text
.NET Application↓Compile / Publish↓Native AOT↓Native Executable↓Operating System↓CPU
Berbeda dengan deployment .NET biasa yang membawa assembly berisi IL untuk kemudian dijalankan oleh runtime dengan JIT, Native AOT menghasilkan output native yang dapat dijalankan tanpa membutuhkan JIT untuk melakukan compilation method pada saat runtime.
Namun tetap terdapat runtime support yang dibutuhkan oleh application.
Jadi:
Plain Text
Native AOT≠No Runtime
Lebih tepat:
Plain Text
Native AOT=Native Code Generated Before Runtime
JIT vs Native AOT
Perbandingan konseptualnya:
Plain Text
JIT Native AOT│ │↓ ↓Build Time Build Time│ │↓ ↓IL Native Code│ │↓ ↓Runtime Runtime│ │JIT ││ │↓ ↓Native Code Execute│ │↓ ↓CPU CPU
Atau dalam bentuk yang lebih ringkas:
Plain Text
JIT:IL → Runtime → JIT → Native Code → CPUAOT:IL → AOT Compiler → Native Code → Runtime → CPU
Perbedaan utamanya adalah lokasi dan waktu compilation.
Apakah AOT Selalu Lebih Baik daripada JIT?
Tidak, AOT bukan pengganti JIT yang selalu lebih baik. Keduanya memiliki trade-off.
JIT memiliki keunggulan:
Plain Text
- Fleksibel- Dapat melakukan optimasi berdasarkan kondisi runtime- Mendukung dynamic behavior dengan lebih natural- Tidak membutuhkan seluruh native compilation saat build
AOT memiliki keunggulan:
Plain Text
- Mengurangi compilation saat runtime- Dapat meningkatkan startup performance pada skenario tertentu- Cocok untuk workload tertentu yang membutuhkan startup cepat- Dapat menghasilkan deployment native yang lebih mandiri
Namun AOT juga memiliki konsekuensi:
Plain Text
- Build/publish dapat lebih mahal- Dynamic code dapat lebih sulit- Reflection dapat membutuhkan perhatian tambahan- Tidak semua library memiliki karakteristik yang sama untuk AOT
Karena itu, pemilihan JIT atau AOT harus disesuaikan dengan kebutuhan application.
JIT dan AOT Bukan Dua Dunia yang Sepenuhnya Terpisah
Perlu diperhatikan bahwa dunia nyata tidak selalu sesederhana:
Plain Text
JIT = semua runtimeAOT = semua compile time
.NET memiliki berbagai strategi compilation.
Terdapat:
Plain Text
JITTiered CompilationReadyToRunNative AOT
dan mekanisme lain yang memungkinkan compilation dilakukan pada tahap yang berbeda.
Salah satu pendekatan yang menarik adalah ReadyToRun (R2R). R2R dapat menyediakan native code yang telah dipersiapkan sebelumnya untuk sebagian code sehingga runtime dapat mengurangi pekerjaan JIT ketika application berjalan.
Secara konseptual:
Plain Text
Build↓Assembly↓Precompiled Native Code↓Runtime↓JIT masih dapat digunakan untuk bagian tertentu
Jadi model execution .NET tidak hanya memiliki dua pilihan ekstrem.
Tidak selalu:
Plain Text
100% JIT
atau:
Plain Text
100% AOT
Terdapat berbagai strategi di antara keduanya.
Tiga Tahap Compilation yang Perlu Dibedakan
Setelah pembahasan JIT dan AOT, terdapat tiga konsep yang perlu dibedakan:
C# Compilation
Mengubah source code menjadi IL dan metadata.
Plain Text
C#↓C# Compiler↓IL + Metadata
JIT Compilation
Mengubah IL menjadi native code ketika runtime membutuhkan code tersebut.
Plain Text
IL↓JIT↓Native Code
AOT Compilation
Menghasilkan native code sebelum application dijalankan.
Plain Text
IL / Intermediate Representation↓AOT Compiler↓Native Code
Ketiganya merupakan proses compilation, tetapi dilakukan pada tahap yang berbeda dan memiliki tujuan yang berbeda.
Bagian 13: Dari Source Code hingga Berjalan
Setelah compiler, IL, dan assembly dibahas secara terpisah, seluruh konsep tersebut perlu dirangkai kembali agar terlihat bagaimana sebuah file source code C# berubah menjadi hasil build yang dapat digunakan oleh .NET.
Contoh paling sederhana adalah program berikut:
C#
public class Program{public static void Main(){Console.WriteLine("Hello World");}}
Kode tersebut disimpan dalam sebuah file:
Plain Text
Program.cs
Pada tahap ini, Program.cs masih merupakan source code. File tersebut hanyalah teks yang mengikuti aturan bahasa C#. CPU belum dapat menjalankannya.
Perjalanan kode tersebut dapat digambarkan secara sederhana:
Plain Text
Program.cs↓C# Compiler↓IL + Metadata↓.NET Assembly↓MyApp.dll
Namun setiap tahap memiliki proses yang berbeda.
1. Source Code: Program.cs
Semuanya dimulai dari source code:
C#
public class Program{public static void Main(){Console.WriteLine("Hello World");}}
Source code berisi konsep-konsep tingkat tinggi seperti:
Plain Text
classmethodstringobjectConsole
Konsep tersebut dibuat agar programmer dapat membangun aplikasi tanpa harus menulis instruksi CPU secara langsung. Pada tahap ini belum ada machine code yang harus dijalankan CPU.
2. C# Compiler Membaca Source Code
Ketika project di-build, C# compiler memproses file .cs.
Secara sederhana:
Plain Text
Program.cs↓C# Compiler
Compiler tidak hanya menerjemahkan setiap baris secara mekanis. Compiler terlebih dahulu menganalisis struktur source code, memeriksa syntax, melakukan type checking, menyelesaikan reference, dan melakukan berbagai analisis lain yang diperlukan untuk menghasilkan output yang valid.
Misalnya jika terdapat:
C#
int number = "Hello";
compiler dapat mendeteksi bahwa "Hello" bukan nilai yang sesuai untuk int.
Jika terdapat kesalahan seperti:
C#
Console.WriteLnie("Hello");
compiler juga dapat menemukan bahwa WriteLnie tidak ditemukan.
Jika proses compilation berhasil, compiler menghasilkan output .NET.
3. Compiler Menghasilkan IL dan Metadata
Berbeda dengan model compiler native yang dapat menghasilkan machine code sebagai output utama, compiler C# umumnya menghasilkan IL beserta metadata.
Secara sederhana:
Plain Text
C# Source Code↓C# Compiler↓┌─────────────────────┐│ IL ││ Metadata ││ Type Information ││ References │└─────────────────────┘
IL berisi instruksi intermediate yang nantinya dapat diproses oleh .NET runtime. Metadata berisi informasi mengenai struktur program, seperti:
Plain Text
Program└── Main()
serta berbagai informasi lain mengenai type, method, parameter, reference, assembly, dan elemen program lainnya.
Karena itu, hasil compilation bukan hanya "kode yang berbeda", tetapi sebuah representasi program yang membawa instruksi dan informasi tentang struktur program.
4. IL dan Metadata Dimasukkan ke Assembly
IL dan metadata tersebut kemudian menjadi bagian dari sebuah assembly.
Secara konseptual:
Plain Text
IL + Metadata↓.NET Assembly
Assembly juga memiliki informasi lain seperti manifest, references, dan resources.
Gambaran sederhananya:
Plain Text
.NET Assembly│├── IL├── Metadata├── Manifest├── Type Information├── References└── Resources
Assembly merupakan unit penting dalam ekosistem .NET karena menjadi wadah kode hasil compilation sekaligus informasi yang diperlukan untuk mengidentifikasi dan menggunakan kode tersebut.
5. Assembly Disimpan sebagai File
Assembly kemudian direpresentasikan sebagai file pada file system.
Misalnya:
Plain Text
MyApp.dll
Jadi, hubungan antara keduanya adalah:
Plain Text
.NET Assembly│▼MyApp.dll
Perlu dibedakan bahwa assembly merupakan konsep atau unit logis dalam .NET, sedangkan MyApp.dll merupakan file fisik yang menyimpan assembly tersebut.
Untuk aplikasi tertentu, hasilnya juga dapat berupa:
Plain Text
MyApp.exe
Seperti yang sudah dibahas pada bagian assembly, .dll dan .exe dapat menjadi bentuk fisik dari .NET assembly dengan peran yang berbeda.
6. Apakah MyApp.dll Sudah Menjadi Machine Code?
Belum.
Ini adalah salah satu bagian terpenting dari seluruh pembahasan. MyApp.dll hasil compilation .NET yang menggunakan model managed code umumnya masih membawa IL.
Gambaran sederhananya:
Plain Text
MyApp.dll│├── IL├── Metadata├── Manifest└── References
IL tersebut bukan machine code x86-64 atau ARM64.
Karena itu, CPU belum dapat mengambil IL dari MyApp.dll lalu menjalankannya secara langsung.
Masih ada tahapan execution.
Plain Text
MyApp.dll↓.NET Runtime↓IL↓JIT↓Native Machine Code↓CPU
Jadi, build dan execution merupakan dua tahap yang berbeda.
Build Tidak Sama dengan Run
Perbedaan ini sangat penting.
Ketika project di-build:
Plain Text
Source Code↓Compiler↓Assembly
Sedangkan ketika aplikasi dijalankan:
Plain Text
Assembly↓.NET Runtime↓Execution
Jika seluruh proses digabungkan:
Plain Text
BUILD│▼Program.cs ──► Compiler│▼MyApp.dll││RUN│▼.NET Runtime│▼JIT│▼Machine Code│▼CPU
Dengan pemisahan tersebut, sebuah source code dapat di-compile terlebih dahulu dan assembly hasilnya kemudian dijalankan pada waktu yang berbeda.
Apa yang Terjadi dengan Main()?
Pada contoh:
C#
public static void Main(){Console.WriteLine("Hello World");}
Main() merupakan entry point aplikasi.
Entry point adalah titik awal eksekusi aplikasi. Namun Main() bukan berarti CPU langsung mencari method bernama Main di dalam source code.
Runtime terlebih dahulu memuat assembly dan mengetahui informasi mengenai entry point berdasarkan metadata dan struktur executable assembly.
Secara konseptual:
Plain Text
MyApp.dll↓Assembly Loading↓Menemukan Entry Point↓Program.Main()↓Eksekusi method
Ketika Main() mulai dijalankan, method tersebut memiliki IL.
IL kemudian dapat dikompilasi oleh JIT menjadi native machine code:
Plain Text
Program.Main()│▼IL│▼JIT│▼Native Machine Code│▼CPU
Barulah CPU menjalankan instruksi native tersebut.
Bagaimana Console.WriteLine() Bisa Berjalan?
Ada satu detail menarik pada contoh ini.
Kode:
C#
Console.WriteLine("Hello World");
terlihat seperti satu perintah sederhana, tetapi sebenarnya melibatkan beberapa komponen.
Console merupakan type yang disediakan oleh library .NET. WriteLine merupakan method yang tersedia pada type tersebut.
String:
Plain Text
"Hello World"
merupakan data yang digunakan sebagai argument.
Secara konseptual, method Main() memanggil method pada library .NET:
Plain Text
Program.Main()││ memanggil▼Console.WriteLine()
Library yang dibutuhkan aplikasi juga merupakan assembly yang dapat dimuat oleh runtime.
Gambaran sederhananya:
Plain Text
MyApp.dll││ menggunakan▼.NET Libraries│▼.NET Runtime│▼CPU
Jadi, sebuah aplikasi .NET biasanya tidak hanya terdiri dari satu assembly.
Mengapa Metadata Dibutuhkan?
Pada saat runtime bekerja, informasi mengenai program tidak hanya berasal dari IL. Runtime juga membutuhkan metadata.
Misalnya terdapat:
C#
public class Program{public static void Main(){Console.WriteLine("Hello World");}}
Metadata membantu mendeskripsikan struktur seperti:
Plain Text
Type:ProgramMethod:MainReturn Type:voidParameters:none
Informasi tersebut tersedia dalam assembly. Hal inilah yang juga menjadi salah satu fondasi kemampuan seperti Reflection. Dengan Reflection, informasi mengenai type dan member dapat diperiksa ketika program sedang berjalan.
Secara sederhana:
Plain Text
Assembly│├── IL│└── Metadata│▼Reflection
Jadi, assembly bukan hanya tempat menyimpan instruksi program. Assembly juga menyimpan informasi yang menjelaskan struktur program tersebut.
Gambaran Lengkap dari Program.cs sampai CPU
Sekarang seluruh perjalanan dapat dirangkai:
Plain Text
┌─────────────────────┐│ Program.cs ││ ││ C# Source Code │└──────────┬──────────┘││ C# Compiler▼┌─────────────────────┐│ IL + Metadata │└──────────┬──────────┘││ packaged as▼┌─────────────────────┐│ MyApp.dll ││ ││ .NET Assembly │└──────────┬──────────┘││ .NET Runtime▼┌─────────────────────┐│ CLR ││ ││ Runtime Services │└──────────┬──────────┘││ JIT▼┌─────────────────────┐│ Native Machine Code │└──────────┬──────────┘│▼┌───────┐│ CPU │└───────┘
Dari diagram tersebut terlihat bahwa source code mengalami beberapa perubahan bentuk sebelum akhirnya dijalankan.
Plain Text
C# Source Code↓IL↓Native Machine Code
Sedangkan assembly menjadi wadah penting pada tahap antara compilation dan execution:
Plain Text
C# Source Code↓Compiler↓.NET Assembly↓Runtime↓CPU
Jangan Membayangkan .dll sebagai "Program yang Sudah Berjalan"
Sebuah kesalahpahaman yang cukup umum adalah menganggap:
Plain Text
Program.cs → MyApp.dll → CPU
Padahal untuk model managed .NET, gambaran tersebut belum lengkap.
Yang lebih tepat:
Plain Text
Program.cs↓Compiler↓MyApp.dll││ berisi IL + Metadata▼.NET Runtime│▼JIT│▼Machine Code│▼CPU
MyApp.dll adalah hasil compilation yang siap diproses oleh .NET runtime.
Ia bukan sekadar source code yang diganti ekstensi, tetapi juga bukan berarti seluruh isinya sudah berupa machine code native.
Satu Program, Banyak Lapisan
Satu baris:
C#
Console.WriteLine("Hello World");
terlihat sederhana karena C# menyembunyikan banyak detail teknis.
Di baliknya terdapat beberapa lapisan:
Plain Text
C# Syntax↓C# Compiler↓IL↓Assembly↓.NET Runtime↓JIT↓Machine Code↓CPU
Setiap lapisan memiliki tanggung jawab sendiri.
C# menyediakan bahasa yang nyaman untuk menulis program. Compiler menerjemahkan source code menjadi bentuk yang dapat digunakan .NET. IL menjadi intermediate representation. Assembly menjadi wadah IL dan metadata. Runtime memuat dan menjalankan assembly. JIT menghasilkan native machine code. CPU menjalankan machine code tersebut.
Bagian 14: Apa Itu .NET SDK?
Setelah memahami .NET Runtime, CLR, JIT, dan AOT, terdapat satu istilah lain yang sangat sering muncul ketika bekerja dengan .NET:
.NET SDK.
Ketika membuat project baru dengan:
Bash
dotnet new console
menjalankan aplikasi:
Bash
dotnet run
atau melakukan build:
Bash
dotnet build
yang digunakan bukan hanya .NET Runtime.
Perintah-perintah tersebut merupakan bagian dari .NET SDK.
Hal ini menimbulkan pertanyaan penting:
Apakah .NET SDK sama dengan .NET Runtime?
Jawabannya:
Tidak.
.NET SDK dan .NET Runtime memiliki tujuan yang berbeda.
Secara sederhana:
Plain Text
.NET SDK↓Membangun aplikasi .NET.NET Runtime↓Menjalankan aplikasi .NET
Perbedaan ini sangat penting untuk memahami apa yang sebenarnya dibutuhkan pada development machine dan apa yang dibutuhkan pada production server.
Apa Itu .NET SDK?
SDK adalah singkatan dari Software Development Kit. Secara umum, SDK adalah kumpulan tools, library, dan komponen yang diperlukan untuk mengembangkan software menggunakan platform tertentu.
Dalam konteks .NET, .NET SDK menyediakan berbagai tools untuk:
Plain Text
Create ProjectBuildCompileTestPublishRunPackageManage Dependencies
Salah satu bagian yang paling sering digunakan adalah command-line interface:
Bash
dotnet
Contohnya:
Bash
dotnet newdotnet restoredotnet builddotnet testdotnet rundotnet publish
Perintah-perintah tersebut merupakan bagian dari tooling yang disediakan oleh .NET SDK.
SDK Bukan Runtime
Perbedaan paling sederhana dapat dilihat dari tujuan masing-masing.
.NET SDK
Digunakan untuk mengembangkan dan membangun aplikasi.
Plain Text
Source Code↓.NET SDK↓Build / Compile / Test / Publish↓Application
.NET Runtime
Digunakan untuk menjalankan aplikasi yang sudah dibangun.
Plain Text
Application↓.NET Runtime↓Execution
Jadi:
Plain Text
SDK↓Build ApplicationRuntime↓Run Application
Keduanya berhubungan, tetapi bukan komponen yang sama.
Apa Saja yang Ada di Dalam .NET SDK?
.NET SDK bukan hanya sebuah executable bernama dotnet. SDK terdiri dari berbagai tools dan komponen yang bekerja bersama untuk proses development.
Secara konseptual:
Plain Text
.NET SDK│├── .NET CLI├── Compiler├── MSBuild├── Workload System├── NuGet tooling├── Templates├── Build targets├── Publishing tools└── SDK libraries
Tidak semua komponen tersebut harus dipahami sekaligus. Beberapa yang paling penting adalah CLI, compiler, MSBuild, NuGet, dan project templates.
.NET CLI
Ketika mengetik:
Bash
dotnet
yang digunakan adalah .NET Command-Line Interface, atau .NET CLI.
CLI menyediakan berbagai command untuk bekerja dengan aplikasi .NET.
Contohnya:
Bash
dotnet newdotnet restoredotnet builddotnet rundotnet testdotnet publish
Masing-masing command memiliki tanggung jawab berbeda.
dotnet new
Digunakan untuk membuat project baru.
Contohnya:
Bash
dotnet new console
akan membuat project console application.
Project kemudian memiliki file seperti:
Plain Text
MyApp/├── MyApp.csproj└── Program.cs
dotnet build
Digunakan untuk melakukan build.
Bash
dotnet build
Secara konseptual:
Plain Text
Source Code↓Compiler↓IL + Metadata↓Assembly
dotnet run
Digunakan untuk menjalankan project.
Secara sederhana:
Bash
dotnet run
dapat dipahami sebagai kombinasi dari proses build dan menjalankan hasil build, meskipun implementasi sebenarnya memiliki detail tambahan seperti incremental build dan dependency handling.
dotnet test
Digunakan untuk menjalankan test.
Bash
dotnet test
dotnet publish
Digunakan untuk menghasilkan output yang siap digunakan untuk deployment.
Bash
dotnet publish
Misalnya:
Plain Text
Project↓Build↓Publish↓Deployment Output
Publish dapat menghasilkan berbagai jenis output tergantung konfigurasi, termasuk deployment framework-dependent, self-contained, atau Native AOT.
Compiler Ada di Dalam SDK
Salah satu komponen paling penting dalam SDK adalah compiler. Untuk C#, compiler yang digunakan adalah Roslyn.
Secara konseptual:
Plain Text
C# Source Code↓Roslyn Compiler↓IL + Metadata↓Assembly
Karena compiler digunakan ketika melakukan build, compiler merupakan bagian dari tooling development.
Hal ini membantu menjelaskan mengapa mesin development biasanya membutuhkan SDK, bukan hanya runtime. Jika hanya tersedia runtime, environment tersebut mungkin dapat menjalankan aplikasi yang sudah jadi, tetapi belum tentu memiliki seluruh tools yang diperlukan untuk mengompilasi source code.
MSBuild
Komponen penting lainnya adalah MSBuild. MSBuild adalah build engine yang digunakan untuk membangun berbagai project .NET.
Ketika menjalankan:
Bash
dotnet build
proses yang terjadi sebenarnya lebih kompleks daripada:
Plain Text
C# → Compiler → DLL
Build system perlu menangani banyak hal:
Plain Text
Project Configuration↓Dependency Resolution↓Restore↓Compilation↓Resource Processing↓Output Generation
MSBuild membantu mengatur proses tersebut.
Konfigurasi project biasanya terdapat dalam file seperti:
Plain Text
MyApp.csproj
Contohnya:
xml
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup></Project>
Bagian:
xml
<Project Sdk="Microsoft.NET.Sdk">
menunjukkan bahwa project menggunakan .NET SDK-style project system.
Project File dan SDK
File .csproj sangat penting dalam ekosistem .NET.
Contohnya:
xml
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework></PropertyGroup></Project>
File tersebut memberikan informasi kepada build system seperti:
Plain Text
Target FrameworkOutput TypeDependenciesBuild ConfigurationPackage ReferencesCompiler Options
Ketika command:
Bash
dotnet build
dijalankan, SDK dan build system membaca konfigurasi project tersebut untuk menentukan bagaimana project harus dibangun.
SDK dan NuGet
SDK juga bekerja bersama NuGet, package manager untuk ekosistem .NET.
Misalnya sebuah project membutuhkan:
xml
<PackageReference Include="Newtonsoft.Json" Version="..." />
Ketika menjalankan:
Bash
dotnet restore
dependency tersebut akan dipulihkan dari package source yang dikonfigurasi.
Secara konseptual:
Plain Text
.csproj↓Package References↓dotnet restore↓NuGet↓Packages↓Build
Jadi SDK bukan hanya compiler. SDK menyediakan tooling yang membantu seluruh proses pembangunan aplikasi.
SDK dan Template
Ketika membuat project:
Bash
dotnet new webapi
SDK tidak mengompilasi source code yang belum ada. Sebaliknya, SDK terlebih dahulu menggunakan template untuk membuat struktur project.
Misalnya:
Bash
dotnet new console
dapat menghasilkan:
Plain Text
MyApp/├── MyApp.csproj└── Program.cs
Template membantu mempercepat proses pembuatan project dengan struktur awal yang sudah ditentukan.
Template juga tersedia untuk berbagai jenis application:
Plain Text
ConsoleClass LibraryWeb APIMVCRazorWorkerTest Project
dan berbagai template lainnya.
SDK dan Runtime dalam Development Machine
Sebuah mesin development biasanya membutuhkan SDK.
Misalnya ketika melakukan:
Bash
dotnet newdotnet builddotnet testdotnet publish
SDK dibutuhkan karena terdapat pekerjaan development yang harus dilakukan.
Gambaran sederhananya:
Plain Text
Developer Machine│└── .NET SDK│├── CLI├── Compiler├── MSBuild├── NuGet├── Templates└── Publishing Tools
SDK juga mencakup atau menggunakan runtime yang diperlukan untuk menjalankan tooling tersebut. Karena itu, ketika SDK terpasang, biasanya environment tersebut juga memiliki komponen runtime yang sesuai.
Bagaimana dengan Production Server?
Production server memiliki kebutuhan yang berbeda. Jika server hanya bertugas menjalankan aplikasi yang sudah dibuild, server tidak selalu membutuhkan SDK.
Contohnya sebuah aplikasi ASP.NET Core sudah dipublish:
Plain Text
Developer Machine↓.NET SDK↓dotnet publish↓Deployment Output↓Production Server↓.NET Runtime↓Application
Server hanya perlu menjalankan application.
Karena itu, untuk deployment framework-dependent, production server umumnya cukup memiliki runtime yang sesuai. Ini juga merupakan salah satu alasan mengapa development environment dan production environment sebaiknya dipisahkan.
Framework-Dependent Deployment
Salah satu model deployment .NET adalah framework-dependent deployment. Dalam model ini, application tidak membawa seluruh .NET runtime sendiri.
Misalnya deployment menghasilkan:
Plain Text
MyApp.dll
Server kemudian membutuhkan .NET runtime yang sesuai.
Gambaran sederhananya:
Plain Text
Server│├── .NET Runtime│└── MyApp.dll
Ketika application dijalankan:
Plain Text
MyApp.dll↓.NET Runtime↓Execution
Dalam model seperti ini, SDK tidak diperlukan hanya untuk menjalankan application.
Self-Contained Deployment
Ada juga model self-contained deployment. Pada model ini, application membawa runtime yang dibutuhkan bersama application.
Secara konseptual:
Plain Text
Application│├── Application├── .NET Runtime└── Dependencies
Sehingga server tidak harus memiliki shared .NET runtime yang terpasang secara terpisah untuk application tersebut.
Gambaran sederhananya:
Plain Text
Developer Machine↓.NET SDK↓dotnet publish↓Self-Contained Application↓Production Server
Ini berbeda dari framework-dependent deployment.
Mengapa dotnet --info Menampilkan SDK dan Runtime?
Ketika menjalankan:
Bash
dotnet --info
informasi yang ditampilkan biasanya mencakup daftar SDK dan runtime yang terpasang.
Misalnya secara konseptual:
Plain Text
.NET SDK:Version: 10.x.x.NET runtimes installed:Microsoft.NETCore.App 10.x.xMicrosoft.AspNetCore.App 10.x.x
Hal ini menunjukkan bahwa SDK dan runtime merupakan komponen yang dapat memiliki versi dan instalasi yang berbeda.
Contohnya sebuah mesin dapat memiliki beberapa SDK:
Plain Text
.NET SDK├── 8.x├── 9.x└── 10.x
dan beberapa runtime:
Plain Text
.NET Runtime├── 8.x├── 9.x└── 10.x
Pemilihan SDK dan runtime yang digunakan oleh sebuah project dapat dipengaruhi oleh konfigurasi project dan environment.
SDK Version Tidak Sama dengan Target Framework
Hal lain yang sering membingungkan adalah perbedaan antara:
Plain Text
SDK Version
dan:
Plain Text
Target Framework
Misalnya project memiliki:
xml
<TargetFramework>net10.0</TargetFramework>
net10.0 menunjukkan target framework application.
Sedangkan SDK yang digunakan untuk membangun project merupakan tooling yang terpasang pada environment development.
Keduanya berkaitan, tetapi bukan hal yang sama.
Secara konseptual:
Plain Text
.NET SDK↓Build Project↓Target Framework: net10.0↓Application
Target framework menjelaskan API/runtime target yang digunakan application, sedangkan SDK menjelaskan tooling yang digunakan untuk membangun application.
global.json
Dalam project .NET, versi SDK yang digunakan dapat dikontrol menggunakan file:
Plain Text
global.json
Contohnya:
JSON
{"sdk": {"version": "10.0.100"}}
File tersebut membantu memastikan project menggunakan versi SDK tertentu ketika terdapat beberapa SDK yang terpasang.
Misalnya sebuah development machine memiliki:
Plain Text
10.0.10010.0.20010.0.300
global.json dapat digunakan untuk menentukan SDK yang diharapkan oleh repository.
Dengan demikian, project tidak sepenuhnya bergantung pada SDK default yang ditemukan di environment.
SDK Bukan Bagian dari Application
Ketika project dibuild:
Bash
dotnet build
SDK digunakan sebagai tooling.
Namun SDK tidak menjadi bagian dari kode application dalam arti bahwa application harus membawa seluruh SDK ke production.
Secara konseptual:
Plain Text
Development────────────.NET SDK↓Build↓MyApp
kemudian:
Plain Text
Production──────────MyApp↓.NET Runtime↓CPU
Production server tidak membutuhkan:
Plain Text
C# CompilerMSBuildProject TemplatesNuGet Restore Tools
jika server tersebut hanya bertugas menjalankan hasil deployment.
Apakah dotnet Itu SDK?
Ini juga perlu diluruskan.
Ketika mengetik:
Bash
dotnet
terlihat seolah-olah dotnet adalah SDK.
Lebih tepatnya, dotnet adalah host/CLI entry point yang digunakan untuk mengakses berbagai kemampuan .NET.
Command seperti:
Bash
dotnet builddotnet rundotnet testdotnet publish
menggunakan tooling yang tersedia dalam SDK.
Sedangkan untuk menjalankan application:
Bash
dotnet MyApp.dll
dotnet juga dapat digunakan sebagai entry point untuk menjalankan application menggunakan runtime.
Jadi satu command:
Plain Text
dotnet
dapat terlihat pada dua konteks:
Plain Text
Developmentdotnet builddotnet testdotnet publishExecutiondotnet MyApp.dll
Perbedaannya adalah apa yang sedang dilakukan dan komponen apa yang digunakan di balik command tersebut.
Bagian 15: Apa yang Terjadi Saat dotnet run?
Command dotnet run mungkin sudah digunakan berkali-kali saat mengembangkan aplikasi .NET. Cukup mengetik satu command, lalu aplikasi berjalan. Namun, di balik command yang terlihat sederhana tersebut sebenarnya terdapat beberapa proses yang saling berhubungan.
dotnet run bukanlah compiler, bukan .NET Runtime, dan bukan CLR. dotnet run adalah bagian dari .NET CLI yang bertugas mengorkestrasi proses agar sebuah project .NET dapat dijalankan.
Secara konseptual, alurnya dapat digambarkan seperti berikut:
Plain Text
dotnet run↓.NET CLI↓Build project jika diperlukan↓Assembly (.dll)↓.NET Host↓.NET Runtime / CLR↓JIT↓Native Machine Code↓Application starts
Agar alur tersebut lebih mudah dipahami, setiap lapisan perlu dilihat secara terpisah.
1. dotnet CLI
dotnet adalah command-line interface yang digunakan untuk berinteraksi dengan .NET.
Contohnya:
Bash
dotnet newdotnet builddotnet rundotnet testdotnet publish
Command-command tersebut bukan bagian dari aplikasi yang sedang dibuat. Command tersebut adalah tooling yang digunakan untuk mengelola project .NET.
Misalnya:
Bash
dotnet build
berarti meminta .NET CLI untuk melakukan proses build.
Sedangkan:
Bash
dotnet run
berarti meminta .NET CLI untuk menyiapkan project dan menjalankan aplikasi.
Dengan demikian, dotnet CLI dapat dianggap sebagai pengatur proses, bukan komponen yang menjalankan kode C# secara langsung.
2. Apa yang Terjadi Saat dotnet run?
Misalkan terdapat project sederhana:
Plain Text
MyApp/├── MyApp.csproj└── Program.cs
Dengan isi:
C#
Console.WriteLine("Hello World");
Ketika menjalankan:
Bash
dotnet run
secara konseptual terjadi beberapa tahap.
Tahap 1 — Project ditemukan
.NET CLI mencari project yang akan dijalankan.
Informasi mengenai project terdapat di file:
Plain Text
MyApp.csproj
File .csproj berisi informasi seperti target framework, dependency, package, dan konfigurasi build.
Contohnya:
xml
<TargetFramework>net10.0</TargetFramework>
Dari informasi tersebut, tooling dapat mengetahui bagaimana project harus dibangun dan dijalankan.
Tahap 2 — Dependency dipersiapkan
Sebelum build, project mungkin membutuhkan package atau dependency dari NuGet.
Misalnya project menggunakan:
xml
<PackageReference Include="SomeLibrary" Version="1.0.0" />
Dependency tersebut perlu tersedia agar compiler dapat membangun project. Proses ini berkaitan dengan restore.
Secara sederhana:
Plain Text
Project↓Restore dependencies↓Dependencies tersedia
Jika dependency sudah tersedia dan tidak ada perubahan yang membutuhkan restore ulang, proses ini dapat memanfaatkan hasil restore sebelumnya.
Tahap 3 — Project di-build
Jika diperlukan, dotnet run akan melakukan build terlebih dahulu.
Secara konseptual:
Plain Text
C# source code↓Compiler↓IL↓Assembly
Misalnya:
Plain Text
Program.cs↓C# Compiler↓MyApp.dll
Compiler C# tidak langsung menghasilkan machine code CPU.
Compiler menghasilkan Intermediate Language (IL) beserta metadata yang kemudian disimpan dalam assembly.
Dengan demikian:
Plain Text
C# source code↓Compiler↓IL↓MyApp.dll
Assembly tersebut belum sama dengan kode machine CPU yang dieksekusi processor.
3. Apa Itu bin?
Setelah proses build, hasil build biasanya ditemukan di folder:
Plain Text
bin/
Contohnya:
Plain Text
MyApp/├── bin/│ └── Debug/│ └── net10.0/│ ├── MyApp.dll│ ├── MyApp.deps.json│ └── MyApp.runtimeconfig.json├── obj/├── MyApp.csproj└── Program.cs
Folder bin berasal dari istilah binary.
Folder ini berisi output hasil build yang nantinya digunakan untuk menjalankan atau mendistribusikan aplikasi, tergantung jenis proses yang dilakukan.
Salah satu file penting di dalamnya adalah:
Plain Text
MyApp.dll
File tersebut merupakan assembly yang berisi IL dan metadata.
Selain .dll, dapat ditemukan file lain seperti:
Plain Text
MyApp.deps.jsonMyApp.runtimeconfig.json
File-file tersebut membantu .NET mengetahui dependency aplikasi dan runtime yang dibutuhkan.
4. Apa Itu obj?
Selain bin, proses build juga menghasilkan folder:
Plain Text
obj/
Folder obj bukan output akhir aplikasi.
Folder ini berisi intermediate build artifacts, yaitu berbagai file sementara atau hasil antara yang dibutuhkan oleh sistem build untuk menyelesaikan proses build.
Contohnya:
Plain Text
obj/├── Debug/│ └── net10.0/│ ├── ref/│ ├── refint/│ ├── MyApp.AssemblyInfo.cs│ └── ...
Beberapa file di dalam obj digunakan untuk:
-
informasi build;
-
generated source code;
-
metadata assembly;
-
reference assemblies;
-
dependency information;
-
incremental build;
-
proses MSBuild lainnya.
Karena itu, obj dapat dibayangkan sebagai area kerja internal proses build, sedangkan bin lebih dekat dengan hasil build yang akan digunakan.
Gambaran sederhananya:
Plain Text
Source Code↓Build↓┌───────────────┐│ obj ││ intermediate ││ artifacts │└───────────────┘↓┌───────────────┐│ bin ││ final build ││ output │└───────────────┘
Secara umum, kedua folder tersebut tidak perlu dimasukkan ke source control seperti Git. Biasanya keduanya masuk ke .gitignore.
5. dotnet build vs dotnet run
dotnet build dan dotnet run sering terlihat mirip karena keduanya dapat menyebabkan proses build. Namun, tujuan akhirnya berbeda.
dotnet build
Command:
Bash
dotnet build
berfokus pada membangun project.
Alurnya kira-kira:
Plain Text
Source Code↓Restore jika diperlukan↓Compiler↓Assembly↓bin/
Setelah selesai, aplikasi tidak otomatis dijalankan.
Contohnya:
Bash
dotnet build
dapat menghasilkan:
Plain Text
Build succeeded.
Tetapi program:
C#
Console.WriteLine("Hello World");
belum dijalankan.
Tujuan utama dotnet build adalah memastikan source code berhasil dikompilasi menjadi output yang dapat digunakan.
6. dotnet run
Sedangkan:
Bash
dotnet run
berfokus pada menjalankan aplikasi.
Secara konseptual:
Plain Text
dotnet run↓Build jika diperlukan↓Application output↓.NET Host↓.NET Runtime↓Application
Karena aplikasi harus memiliki hasil build terlebih dahulu, dotnet run dapat melakukan build sebagai bagian dari prosesnya.
Setelah output tersedia, .NET Host digunakan untuk memulai aplikasi.
Jadi:
Plain Text
dotnet build
berarti:
"Bangun aplikasi."
Sedangkan:
Plain Text
dotnet run
berarti:
"Siapkan aplikasi jika diperlukan, lalu jalankan."
7. dotnet build Tidak Sama dengan dotnet publish
Ada satu command lain yang sering disalahartikan sebagai versi lain dari build:
Bash
dotnet publish
Padahal tujuan publish berbeda.
Secara sederhana:
Plain Text
build↓Menghasilkan output untuk kebutuhan build/runpublish↓Menyiapkan aplikasi untuk deployment
build terutama digunakan selama proses pengembangan dan validasi bahwa project dapat dikompilasi.
Sedangkan publish menyiapkan sekumpulan file yang diperlukan untuk menjalankan aplikasi di environment tujuan.
Contohnya:
Bash
dotnet publish -c Release
Hasilnya dapat berada di:
Plain Text
bin/└── Release/└── net10.0/└── publish/├── MyApp.dll├── MyApp.deps.json├── MyApp.runtimeconfig.json└── ...
Folder publish inilah yang biasanya menjadi sumber file untuk deployment.
8. Mengapa publish Dibutuhkan?
Misalkan sebuah aplikasi dibuat di komputer development:
Plain Text
Developer Machine↓Source Code↓Build↓MyApp.dll
Aplikasi tersebut kemudian ingin dijalankan di server.
Server tidak membutuhkan source code seperti:
Plain Text
Program.csSomeService.csUserController.cs
Yang dibutuhkan adalah hasil aplikasi beserta dependency yang diperlukan untuk menjalankannya.
Karena itu dilakukan:
Bash
dotnet publish
Secara konseptual:
Plain Text
Source Code↓Build↓Publish↓Deployment-ready output↓Server
Jadi publish bukan sekadar "build lagi". publish adalah proses menyiapkan output aplikasi untuk digunakan di environment target.
9. Perbedaan build dan publish
Perbedaan sederhananya dapat dirangkum seperti berikut:
| Command | Tujuan |
|---|---|
dotnet build | Mengompilasi project |
dotnet run | Build jika diperlukan lalu menjalankan aplikasi |
dotnet publish | Menyiapkan aplikasi untuk deployment |
Contoh alurnya:
Plain Text
Development│├── dotnet build│ ↓│ Memastikan kode dapat dibangun│├── dotnet run│ ↓│ Menjalankan aplikasi│└── dotnet publish↓Menyiapkan aplikasiuntuk deployment
10. Build Output vs Publish Output
Perbedaan ini penting ketika bekerja dengan deployment.
Hasil build dapat berupa:
Plain Text
bin/└── Release/└── net10.0/├── MyApp.dll├── MyApp.deps.json└── MyApp.runtimeconfig.json
Sedangkan hasil publish biasanya berada di:
Plain Text
bin/└── Release/└── net10.0/└── publish/├── MyApp.dll├── MyApp.deps.json├── MyApp.runtimeconfig.json└── dependency lainnya
Publish output dirancang sebagai output yang lebih siap untuk dipindahkan ke environment tujuan.
Misalnya:
Plain Text
Developer Computer↓dotnet publish↓publish/↓Server↓Application starts
11. Setelah MyApp.dll Dibuat, Siapa yang Menjalankannya?
Sampai tahap build, compiler sudah menyelesaikan pekerjaannya.
Misalnya:
Plain Text
Program.cs↓C# Compiler↓MyApp.dll
Tetapi MyApp.dll masih berisi IL, bukan native instruction yang langsung dijalankan CPU.
Di sinilah .NET Host dan .NET Runtime berperan.
Secara konseptual:
Plain Text
MyApp.dll↓.NET Host↓.NET Runtime / CLR↓JIT↓Native Code↓CPU
12. Apa Peran .NET Host?
.NET Host bertugas memulai aplikasi .NET dan menentukan runtime yang perlu digunakan.
Ketika aplikasi dijalankan, host membantu menemukan dan memuat runtime yang sesuai berdasarkan informasi aplikasi.
File:
Plain Text
MyApp.runtimeconfig.json
berisi informasi yang membantu proses tersebut.
Jadi, host dapat dipandang sebagai pintu masuk untuk memulai aplikasi .NET.
Perlu dibedakan:
Plain Text
.NET CLI
dengan:
Plain Text
.NET Host
.NET CLI digunakan untuk menjalankan command seperti:
Bash
dotnet builddotnet rundotnet publish
Sedangkan .NET Host berhubungan dengan proses memulai aplikasi dan runtime.
13. Apa yang Terjadi Jika Build Tidak Diperlukan?
Tidak berarti setiap menjalankan:
Bash
dotnet run
selalu melakukan kompilasi penuh dari awal. Sistem build .NET mendukung incremental build. Artinya, sistem dapat menentukan apakah perubahan tertentu memang membutuhkan build ulang.
Misalnya source code tidak berubah:
Plain Text
Program.cs↓Tidak berubah↓Output build masih valid
Maka tidak selalu diperlukan kompilasi ulang seluruh project.
Jika source code berubah:
Plain Text
Program.cs berubah↓Build diperlukan↓Compiler berjalan↓Output diperbarui
Mekanisme ini membuat proses development jauh lebih efisien dibandingkan selalu membangun seluruh project dari awal.
14. Mengapa obj dan bin Sering Muncul dan Hilang?
Saat menjalankan:
Bash
dotnet build
atau:
Bash
dotnet run
folder berikut biasanya muncul:
Plain Text
bin/obj/
Hal ini normal.
Jika kedua folder tersebut dihapus:
Bash
rm -rf bin obj
atau di Windows:
powershell
Remove-Item bin,obj -Recurse -Force
project masih dapat dibangun kembali.
Saat build berikutnya dilakukan, .NET akan membuat kembali folder tersebut. Inilah sebabnya bin dan obj biasanya tidak dianggap sebagai source code project.
Source code utama:
Plain Text
Program.csMyApp.csprojControllers/Services/Models/
sedangkan:
Plain Text
bin/obj/
merupakan hasil dari proses build.
15. Kenapa bin dan obj Kadang Perlu Dihapus?
Dalam development, terkadang terjadi kondisi ketika hasil build sebelumnya menyebabkan masalah.
Misalnya terdapat:
Plain Text
obj/bin/
yang berisi hasil build lama.
Salah satu cara untuk melakukan clean build adalah:
Bash
dotnet cleandotnet build
Atau dalam kasus tertentu, folder bin dan obj dapat dihapus secara manual kemudian project dibangun kembali.
Konsepnya:
Plain Text
Old build artifacts↓Remove↓Build↓Fresh build artifacts
Hal ini sering membantu ketika terjadi masalah yang berkaitan dengan stale build artifacts atau hasil intermediate yang tidak lagi sesuai dengan kondisi source code.
Bagian 16: Apa yang Sebenarnya Berjalan di Server?
Setelah memahami proses dotnet run, build, assembly, runtime, CLR, dan JIT, muncul pertanyaan penting berikutnya:
Ketika aplikasi ASP.NET Core sudah selesai dibuat dan di-deploy ke server, sebenarnya apa yang dijalankan oleh server?
Jawabannya cukup sederhana:
Server tidak menjalankan Program.cs.
File seperti:
Plain Text
Program.csUserController.csOrderService.csProductRepository.cs
adalah source code. Source code digunakan pada tahap development dan build, bukan sebagai bentuk kode yang langsung dieksekusi oleh CPU.
Setelah aplikasi selesai dibuild dan dipublish, yang dikirim ke server adalah application output beserta dependency dan file konfigurasi yang diperlukan untuk menjalankan aplikasi.
Salah satu file terpenting adalah:
Plain Text
MyApp.dll
File tersebut merupakan assembly yang dihasilkan dari proses compilation.
1. Dari Source Code ke Server
Misalkan terdapat aplikasi ASP.NET Core dengan struktur:
Plain Text
MyApp/├── Program.cs├── Controllers/│ └── ProductController.cs├── Services/│ └── ProductService.cs├── Models/│ └── Product.cs└── MyApp.csproj
Pada komputer development, file-file tersebut merupakan source code.
Kemudian dilakukan:
Bash
dotnet publish -c Release
Secara konseptual:
Plain Text
Source Code↓Compiler↓IL + Metadata↓Assembly↓Publish↓Publish Output↓Server
Hasil publish dapat berisi sesuatu seperti:
Plain Text
publish/├── MyApp.dll├── MyApp.deps.json├── MyApp.runtimeconfig.json├── appsettings.json├── dependency.dll└── ...
Yang dipindahkan ke server adalah hasil publish tersebut.
Source code seperti:
Plain Text
Program.csProductController.csProductService.cs
tidak diperlukan untuk menjalankan aplikasi dalam deployment normal.
2. Jadi, Apakah Program.cs Ada di Server?
Dalam deployment normal, tidak harus ada. Program.cs digunakan oleh compiler untuk menghasilkan assembly.
Misalnya:
C#
var builder = WebApplication.CreateBuilder(args);var app = builder.Build();app.MapGet("/", () => "Hello World");app.Run();
Kode tersebut diproses oleh compiler.
Secara sederhana:
Plain Text
Program.cs↓C# Compiler↓IL + Metadata↓MyApp.dll
Setelah MyApp.dll tersedia, runtime tidak perlu membuka Program.cs lalu membaca baris:
C#
app.Run();
Runtime menjalankan hasil compilation yang terdapat dalam assembly.
Dengan kata lain:
Plain Text
Program.cs↓digunakan untuk membuat↓MyApp.dll↓yang digunakan saat aplikasi berjalan
3. Server Menjalankan Assembly
Pada saat aplikasi dijalankan di server, salah satu entry point-nya adalah:
Plain Text
MyApp.dll
Kemudian .NET Host dan runtime mempersiapkan environment untuk menjalankan assembly tersebut.
Secara konseptual:
Plain Text
MyApp.dll↓.NET Host↓.NET Runtime↓CLR / Runtime Services↓JIT↓Native Machine Code↓CPU
Perlu diperhatikan bahwa MyApp.dll bukan native executable seperti .exe Windows tradisional. Assembly .NET berisi IL dan metadata. JIT kemudian mengubah IL menjadi native machine code yang dapat dieksekusi oleh CPU.
4. Apa yang Terjadi Saat Server Menjalankan MyApp.dll?
Misalnya server menjalankan:
Bash
dotnet MyApp.dll
Command tersebut berarti meminta .NET untuk menjalankan assembly:
Plain Text
MyApp.dll
Alurnya secara konseptual:
Plain Text
dotnet MyApp.dll↓.NET Host↓Memuat MyApp.dll↓Mencari runtime yang sesuai↓CLR / Runtime↓Memuat managed assembly↓JIT↓Native Code↓CPU
Pada titik ini, server tidak perlu memiliki:
Plain Text
Program.cs
karena informasi yang diperlukan untuk menjalankan aplikasi sudah terdapat dalam assembly dan file deployment lainnya.
5. Lalu Apa yang Terjadi dengan Program.cs?
Program.cs bukan berarti "hilang" begitu saja. Kode di dalamnya telah dikompilasi menjadi bentuk yang terdapat dalam assembly.
Misalnya terdapat:
C#
app.MapGet("/hello", () => "Hello");
Compiler menerjemahkan kode tersebut menjadi representasi IL dan metadata.
Secara konseptual:
Plain Text
C#:app.MapGet("/hello", ...)↓ CompilerIL:instruksi intermediate↓ disimpan dalamMyApp.dll
Ketika aplikasi berjalan:
Plain Text
MyApp.dll↓Runtime↓JIT↓Native instructions↓CPU
Jadi tidak ada proses:
Plain Text
Server↓Buka Program.cs↓Baca kode C#↓Jalankan
Yang terjadi adalah:
Plain Text
Server↓Load MyApp.dll↓Runtime↓JIT↓Native Code↓CPU
6. Apakah Semua Isi Source Code Hilang dari Assembly?
Tidak selalu sesederhana "source code berubah menjadi IL lalu seluruh informasi source code hilang". Assembly juga dapat mengandung berbagai metadata.
Metadata dapat menyimpan informasi mengenai:
-
type;
-
method;
-
property;
-
parameter;
-
assembly reference;
-
attribute;
-
dan informasi lain yang diperlukan oleh runtime.
Dalam kondisi tertentu, debug symbols seperti file .pdb juga dapat tersedia.
Debug symbols membantu proses debugging dengan menghubungkan instruction hasil compilation dengan informasi source code. Namun, keberadaan informasi debugging tersebut tidak berarti server menjalankan file .cs. Yang dieksekusi tetap merupakan compiled assembly dan native code hasil runtime compilation.
7. Apakah Server Harus Memiliki .NET Runtime?
Untuk deployment framework-dependent, ya. Jika aplikasi dipublish sebagai framework-dependent deployment, server harus memiliki runtime .NET yang sesuai.
Misalnya:
Plain Text
MyApp.dll↓Membutuhkan .NET Runtime↓Server harus menyediakan runtime
Kemudian:
Plain Text
MyApp.dll↓.NET Runtime↓CLR↓JIT↓CPU
Inilah salah satu alasan environment server perlu dipersiapkan dengan runtime .NET yang sesuai dengan target aplikasi.
8. Bagaimana dengan Self-Contained Deployment?
Ada jenis deployment lain yang disebut self-contained deployment. Pada pendekatan ini, aplikasi dipublish bersama runtime yang diperlukan.
Secara konseptual:
Plain Text
Framework-Dependent────────────────────Server├── .NET Runtime└── MyApp
Sedangkan:
Plain Text
Self-Contained────────────────────Server└── MyApp├── Application└── Required .NET Runtime
Dengan self-contained deployment, server tidak harus memiliki shared .NET runtime yang sudah terpasang secara terpisah untuk aplikasi tersebut.
Namun, self-contained bukan berarti source code dikirim ke server.
Tetap:
Plain Text
Source Code↓Build↓Publish↓Application Output↓Server
Perbedaannya terutama terletak pada bagaimana runtime .NET disediakan.
9. Bagaimana ASP.NET Core Bisa Menerima HTTP Request?
Sampai titik ini, aplikasi baru terlihat seperti:
Plain Text
MyApp.dll↓Runtime↓CPU
Tetapi aplikasi ASP.NET Core sebenarnya harus melakukan sesuatu yang lebih konkret: menunggu dan menangani HTTP request.
Misalnya browser mengirim:
http
GET /products/123
Request tersebut masuk melalui jaringan ke server.
Secara sederhana:
Plain Text
Browser││ HTTP Request▼Server│▼ASP.NET Core Application│▼Middleware / Routing│▼Controller / Endpoint│▼Application Logic│▼Response│▼Browser
Namun semua logic tersebut tetap pada akhirnya dijalankan sebagai machine code oleh CPU.
Misalnya sebuah controller:
C#
[HttpGet("{id}")]public IActionResult Get(int id){var product = service.GetProduct(id);return Ok(product);}
Server tidak membaca file:
Plain Text
ProductController.cs
ketika request datang.
Method tersebut sudah menjadi bagian dari compiled assembly. Ketika method diperlukan, runtime dapat menjalankan code yang berasal dari assembly tersebut.
10. Request Datang Setelah Aplikasi Sudah Berjalan
Hal penting lainnya adalah membedakan application startup dengan request processing.
Saat aplikasi pertama kali dimulai:
Plain Text
dotnet MyApp.dll↓.NET Host↓Runtime↓Application startup↓Web server mulai listening
Misalnya aplikasi kemudian listening pada:
Plain Text
http://localhost:5000
Setelah itu aplikasi berada dalam kondisi menunggu request.
Plain Text
Application││ waiting▼HTTP Request│▼ASP.NET Core│▼Application Code│▼HTTP Response
Aplikasi tidak melakukan compilation dari source code setiap kali HTTP request datang.
Code sudah tersedia dalam bentuk compiled assembly dan runtime akan menjalankan code tersebut ketika diperlukan.
11. Apakah JIT Melakukan Compilation Saat Setiap Request?
Tidak. Ini merupakan kesalahpahaman yang cukup umum.
JIT tidak berarti:
Plain Text
Request 1↓Compile seluruh aplikasiRequest 2↓Compile seluruh aplikasi lagiRequest 3↓Compile seluruh aplikasi lagi
Bukan seperti itu.
JIT bekerja ketika method perlu dieksekusi dan native code yang sesuai belum tersedia. Setelah code tersebut dikompilasi oleh JIT, hasilnya dapat digunakan kembali sesuai mekanisme runtime.
Secara sederhana:
Plain Text
IL↓JIT↓Native Code↓Execute↓Reuse
Detail mekanisme JIT jauh lebih kompleks, tetapi konsep utamanya adalah bahwa JIT merupakan bagian dari proses runtime compilation, bukan compiler yang membaca ulang source code setiap request.
12. Apakah Server Bisa Menjalankan Source Code?
Secara teknis, source code dapat saja berada di server dan dapat digunakan oleh tool tertentu dalam situasi khusus. Namun, source code bukan sesuatu yang diperlukan untuk menjalankan aplikasi ASP.NET Core dalam deployment normal.
Deployment normal lebih menyerupai:
Plain Text
Developer Machine││ source code▼Build│▼Publish││ application artifacts▼Server│▼MyApp.dll│▼Runtime
Karena itu, source code dan application output perlu dibedakan.
Plain Text
Source Code↓Input untuk compilationAssembly↓Output dari compilation↓Digunakan oleh runtime
13. Source Code vs Assembly vs Native Code
Tiga bentuk ini merupakan konsep yang berbeda.
Source Code
Contoh:
C#
Console.WriteLine("Hello");
Bentuk ini dibuat dan dibaca oleh programmer.
IL / Assembly
Setelah compilation:
Plain Text
Program.cs↓Compiler↓IL + Metadata↓MyApp.dll
Bentuk ini digunakan oleh .NET runtime.
Native Machine Code
Saat runtime:
Plain Text
IL↓JIT↓Native Machine Code
Bentuk inilah yang akhirnya dapat dieksekusi secara langsung oleh CPU.
Jadi:
Plain Text
Source Code↓C#↓Compiler↓IL↓Assembly↓JIT↓Native Code↓CPU
14. Lalu Apa yang Sebenarnya "Berjalan"?
Pertanyaan ini memiliki beberapa jawaban tergantung level abstraksi.
Jika dilihat dari level aplikasi:
Yang dijalankan adalah assembly .NET seperti MyApp.dll.
Jika dilihat dari level runtime:
CLR/runtime menjalankan managed code yang terdapat dalam assembly.
Jika dilihat dari level compiler:
IL diterjemahkan oleh JIT menjadi native machine code.
Jika dilihat dari level CPU:
CPU mengeksekusi native machine instructions.
Semua jawaban tersebut benar karena masing-masing melihat proses dari lapisan yang berbeda.
Gambaran lengkapnya:
Plain Text
Application Level│▼MyApp.dll│▼.NET Runtime│▼CLR│▼JIT│▼Native Machine Code│▼CPU
15. Dari Development Sampai Production
Seluruh perjalanan aplikasi .NET dapat dirangkum menjadi satu alur besar:
Plain Text
DEVELOPMENT│▼C# Source Code│▼dotnet build│▼C# Compiler│▼IL + Metadata│▼MyApp.dll│▼dotnet publish│▼Publish Output│▼SERVER│▼.NET Host│▼.NET Runtime│▼CLR│▼JIT│▼Native Code│▼CPU
Ketika aplikasi ASP.NET Core menerima request:
Plain Text
Browser││ HTTP Request▼Server│▼ASP.NET Core│▼Application Code│▼CPU executes native code│▼HTTP Response│▼Browser
16. Jadi, Apa yang Dikirim Saat Deployment?
Hal ini dapat diringkas dengan perbandingan berikut:
| Development | Deployment |
|---|---|
.cs source code | Compiled assembly |
.csproj | Application configuration |
| Source files | .dll |
| Development dependencies | Runtime dependencies |
obj/ | Publish output |
bin/ | Publish output |
Tentu saja isi deployment dapat berbeda tergantung jenis aplikasi dan konfigurasi publish.
Untuk ASP.NET Core, publish output dapat berisi:
Plain Text
publish/├── MyApp.dll├── MyApp.deps.json├── MyApp.runtimeconfig.json├── appsettings.json├── dependency.dll└── ...
File-file tersebut kemudian digunakan untuk menjalankan aplikasi di server.
Bagian 17: Apa Itu Server?
Kata server sering terdengar seperti sesuatu yang berbeda jauh dari komputer yang digunakan sehari-hari. Ketika mendengar istilah server, sering muncul bayangan sebuah mesin besar yang berada di dalam data center, memiliki banyak CPU dan RAM, lalu digunakan untuk menjalankan aplikasi. Padahal, secara konsep, server bukanlah jenis komputer yang memiliki cara kerja fundamental berbeda. Server pada dasarnya adalah komputer yang menjalankan software untuk menyediakan layanan kepada komputer atau aplikasi lain.
Layanan tersebut bisa bermacam-macam. Sebuah komputer dapat berperan sebagai web server ketika menjalankan software yang menerima HTTP request dan mengirimkan HTTP response. Komputer lain dapat berperan sebagai database server ketika menjalankan database seperti SQL Server atau PostgreSQL. Ada pula server yang menyediakan file, authentication, message queue, cache, dan berbagai layanan lainnya.
Dengan demikian, kata server lebih menggambarkan peran sebuah komputer daripada jenis hardware-nya. Sebuah laptop pun sebenarnya dapat berperan sebagai server. Misalnya sebuah aplikasi ASP.NET Core dijalankan menggunakan dotnet run, kemudian aplikasi tersebut melakukan listening pada port tertentu. Ketika browser mengakses port tersebut, laptop menerima request dan memberikan response. Dalam situasi tersebut, laptop sedang menjalankan peran sebagai server.
Plain Text
Browser(Client)││ HTTP Request▼Laptop(Server)││ ASP.NET Core▼HTTP Response
Hal yang sama berlaku untuk komputer yang digunakan di production. Perbedaannya bukan karena komputer tersebut tiba-tiba menjadi jenis komputer yang berbeda, tetapi karena komputer tersebut dikonfigurasi dan digunakan untuk menjalankan workload tertentu. Production server biasanya dirancang untuk menangani workload yang lebih besar, tersedia lebih lama, memiliki reliability lebih tinggi, dan dapat dikelola sebagai bagian dari infrastructure yang lebih kompleks.
Karena itu, server tidak harus selalu berbentuk physical machine. Dalam lingkungan tradisional, server memang dapat berupa komputer fisik yang ditempatkan di data center. Komputer tersebut memiliki CPU, RAM, storage, network interface, dan operating system seperti komputer lainnya.
Plain Text
Physical Server├── CPU├── RAM├── Storage├── Network Interface└── Operating System
Perbedaannya biasanya terletak pada skala dan karakteristik hardware. Server enterprise dapat memiliki puluhan atau bahkan lebih banyak CPU core, RAM dalam jumlah besar, storage berperforma tinggi, network interface berkecepatan tinggi, serta berbagai mekanisme redundancy. Tujuannya bukan agar CPU dapat menjalankan jenis instruction yang berbeda, tetapi agar komputer mampu menangani workload yang besar dan tetap tersedia ketika terjadi masalah pada salah satu komponennya.
Server juga dapat berbentuk Virtual Machine. Dalam virtualisasi, satu physical server dapat digunakan untuk menjalankan beberapa virtual machine. Setiap VM terlihat seperti sebuah komputer dari sudut pandang operating system di dalamnya.
Plain Text
Physical Server│▼Hypervisor│┌─────┼─────┐▼ ▼ ▼VM 1 VM 2 VM 3
Setiap VM dapat memiliki operating system, CPU virtual, RAM virtual, storage virtual, dan network interface virtual. Namun hardware tersebut sebenarnya disediakan oleh physical machine melalui virtualization layer. Dengan cara ini, satu physical server dapat menjalankan beberapa environment yang terisolasi.
Konsep yang lebih ringan lagi adalah container. Container biasanya tidak membawa operating system lengkap seperti virtual machine. Container berbagi kernel operating system dengan host, tetapi proses dan filesystem aplikasi dapat diisolasi sehingga aplikasi dapat dikemas bersama dependency yang dibutuhkan.
Plain Text
Operating System│▼Container Runtime│┌───┼───┐▼ ▼ ▼C1 C2 C3
Misalnya sebuah aplikasi ASP.NET Core dapat dikemas ke dalam container bersama file seperti MyApp.dll dan dependency yang diperlukan. Container tersebut kemudian dapat dijalankan pada sebuah physical server, virtual machine, atau infrastructure cloud.
Cloud kemudian menambahkan tingkat abstraksi yang lebih tinggi. Sebuah cloud instance pada dasarnya dapat terlihat seperti sebuah virtual machine biasa, tetapi hardware fisik yang berada di bawahnya dikelola oleh cloud provider. Developer cukup menentukan kebutuhan seperti CPU, RAM, storage, dan network, sementara detail physical infrastructure tidak perlu dikelola secara langsung.
Tingkat abstraksi bahkan dapat dibuat lebih tinggi lagi melalui managed cloud platform. Pada model seperti ini, operating system, runtime, networking, scaling, dan berbagai bagian infrastructure dapat dikelola oleh provider. Developer cukup memberikan application output atau container image, kemudian platform menangani sebagian besar detail infrastructure di belakangnya.
Meskipun bentuk server dapat berbeda-beda, ada satu hal yang tetap sama: CPU pada akhirnya tetap menjalankan native machine instructions.
Pembahasan pada bagian sebelumnya telah menunjukkan perjalanan kode dari source code hingga CPU:
Plain Text
C# Source Code↓Compiler↓IL↓Assembly↓Runtime↓JIT↓Native Machine Code↓CPU
Alur tersebut tidak berubah hanya karena aplikasi dipindahkan dari laptop ke server. Jika aplikasi ASP.NET Core dijalankan pada laptop, native machine code tetap dieksekusi oleh CPU laptop. Jika aplikasi yang sama dijalankan pada server, native machine code tetap dieksekusi oleh CPU server.
Yang biasanya berubah adalah kapasitas dan konfigurasi infrastructure.
Laptop mungkin memiliki:
Plain Text
8 CPU cores16 GB RAMSSD
Sedangkan production server mungkin memiliki:
Plain Text
32 CPU cores128 GB RAMHigh-performance storageHigh-bandwidth network
Namun secara fundamental, CPU tetap melakukan hal yang sama: mengambil dan mengeksekusi machine instructions.
Perbedaan tersebut menjadi penting ketika aplikasi mulai menerima banyak request. Sebuah aplikasi yang hanya digunakan oleh beberapa orang mungkin cukup dijalankan pada satu komputer. Ketika jumlah pengguna meningkat, server membutuhkan resource yang lebih besar atau jumlah server yang lebih banyak.
Ada dua pendekatan umum untuk meningkatkan kapasitas. Pendekatan pertama adalah vertical scaling, yaitu menambahkan resource pada server yang sama. CPU, RAM, atau storage dapat ditingkatkan sehingga satu server mampu menangani workload yang lebih besar.
Plain Text
Server↓Tambah CPUTambah RAMTambah Storage
Pendekatan kedua adalah horizontal scaling, yaitu menambahkan lebih banyak server atau application instance.
Plain Text
┌── Application Server 1│Client → Load Balancer ── Application Server 2│└── Application Server 3
Dalam arsitektur seperti ini, load balancer dapat mendistribusikan request ke beberapa application server. Setiap application server menjalankan aplikasi yang sama, tetapi workload dibagi di antara beberapa instance.
Pada sistem yang lebih kompleks, server juga dapat dipisahkan berdasarkan tanggung jawabnya. Misalnya terdapat application server yang menjalankan ASP.NET Core, database server yang menjalankan SQL Server, dan cache server yang menjalankan Redis.
Plain Text
Client│▼Application Server│├── Database Server│└── Cache Server
Masing-masing tetap merupakan komputer atau environment yang menjalankan software tertentu. Istilah application server, database server, dan cache server terutama menjelaskan layanan yang diberikan oleh environment tersebut.
Hal yang menarik adalah satu komputer juga dapat memiliki beberapa peran sekaligus. Sebuah komputer dapat menjalankan web server, application server, database, dan background worker secara bersamaan. Pada sistem production yang besar, layanan-layanan tersebut biasanya dipisahkan untuk mendapatkan isolation, scalability, security, dan deployment yang lebih baik.
Karena itu, client dan server juga sebenarnya merupakan peran dalam sebuah komunikasi, bukan label permanen sebuah komputer. Sebuah komputer dapat menjadi client terhadap komputer lain, sekaligus menjadi server bagi komputer yang berbeda.
Plain Text
Computer A││ Request▼Computer B││ Response▼Computer A
Dalam komunikasi tersebut, Computer A adalah client dan Computer B adalah server. Namun Computer A juga dapat menerima request dari Computer C dan kemudian berperan sebagai server.
Plain Text
Computer C││ Request▼Computer A││ Response▼Computer C
Jadi istilah client dan server lebih tepat dipahami berdasarkan hubungan antara dua pihak dalam suatu komunikasi.
Dari sini terlihat bahwa tidak ada tahap di mana server membaca dan menjalankan Program.cs secara langsung. Source code digunakan pada tahap development dan compilation. Setelah itu, hasil compilation berupa assembly dipublish dan ditempatkan pada environment yang akan menjalankan aplikasi.
Server kemudian menyediakan environment yang dibutuhkan agar application process dapat berjalan. Operating system mengelola process dan resource, .NET Runtime menyediakan environment untuk managed code, JIT menghasilkan native machine code ketika diperlukan, dan CPU mengeksekusi instruction tersebut.
Dengan demikian, server sebenarnya bukan "komputer ajaib" yang memiliki cara kerja berbeda dari komputer biasa. Server adalah komputer, virtual machine, container, atau managed environment yang menjalankan software untuk menyediakan layanan kepada pihak lain.
Perbedaan terbesar antara komputer development dan production server biasanya terletak pada skala, konfigurasi, reliability, security, networking, dan bagaimana infrastructure tersebut dikelola.
Pada level paling rendah, prosesnya tetap kembali kepada konsep yang sama yang sudah dibahas sejak awal:
Plain Text
Software↓Native Machine Code↓CPU
Server hanya menyediakan tempat dan infrastructure agar proses tersebut dapat berjalan sebagai sebuah layanan yang dapat digunakan oleh aplikasi atau pengguna lain.
Bagian 18: Physical Machine
Bentuk server yang paling mudah dibayangkan adalah physical machine, yaitu komputer yang benar-benar terdiri dari hardware fisik. Di dalamnya terdapat CPU, memory, storage, network interface, dan berbagai komponen hardware lainnya. Komputer seperti ini dapat digunakan sebagai server ketika menjalankan software yang menyediakan layanan kepada komputer atau aplikasi lain.
Gambaran sederhananya dapat dilihat seperti berikut:
Plain Text
Physical Server│├── CPU├── Memory├── Storage└── Network│▼Operating System│▼.NET Runtime│▼ASP.NET Core Application
Gambaran tersebut menunjukkan bahwa aplikasi ASP.NET Core sebenarnya hanya berada pada salah satu lapisan paling atas. Di bawah aplikasi masih terdapat beberapa lapisan lain yang menyediakan kemampuan agar aplikasi dapat berjalan.
Pada lapisan paling bawah terdapat hardware. CPU bertugas mengeksekusi machine instructions, memory digunakan untuk menyimpan data dan program yang sedang aktif, storage digunakan untuk menyimpan data secara persisten, sedangkan network interface memungkinkan komputer berkomunikasi dengan komputer lain melalui jaringan.
Hardware tersebut tidak secara langsung memahami konsep seperti "ASP.NET Core", "C#", atau bahkan ".NET". CPU hanya memahami instruction set yang didukung oleh processor tersebut. Ketika aplikasi .NET akhirnya dijalankan, instruction yang dieksekusi CPU tetap berupa native machine instructions.
Plain Text
Application↓.NET Runtime↓Native Machine Code↓CPU
Di atas hardware terdapat Operating System, seperti Windows atau Linux. Operating system memiliki tanggung jawab yang jauh lebih mendasar daripada aplikasi.
Operating system mengelola resource hardware dan menyediakan berbagai layanan yang dapat digunakan oleh application process. CPU, memory, storage, network, process, thread, file, dan berbagai resource lainnya dikelola melalui mekanisme operating system.
Karena itu, aplikasi tidak perlu mengetahui bagaimana cara mengendalikan hardware tertentu secara langsung.
Misalnya aplikasi ASP.NET Core ingin membaca sebuah file. Aplikasi tidak perlu mengetahui detail bagaimana SSD bekerja secara elektrik atau bagaimana controller storage mengirimkan data. Aplikasi menggunakan filesystem API yang disediakan operating system, kemudian operating system menangani interaksi dengan hardware.
Gambaran sederhananya:
Plain Text
ASP.NET Core↓.NET Runtime↓Operating System↓Storage Driver / Filesystem↓Storage Hardware
Hal yang sama terjadi pada network. Ketika ASP.NET Core menerima HTTP request, aplikasi tidak berkomunikasi langsung dengan kabel jaringan atau network controller. Request melewati berbagai lapisan networking yang dikelola oleh operating system sebelum akhirnya diterima oleh application.
Plain Text
Network↓Network Hardware↓Operating System↓.NET / ASP.NET Core↓Application
Dengan demikian, operating system dapat dipandang sebagai penghubung utama antara software dengan hardware, sekaligus sebagai pengelola resource komputer.
Di atas operating system kemudian terdapat .NET Runtime. Runtime tidak menggantikan operating system. Runtime justru berjalan menggunakan fasilitas yang disediakan oleh operating system.
Secara konseptual:
Plain Text
Hardware↓Operating System↓.NET Runtime↓Application
Hubungan ini penting untuk dipahami karena terkadang muncul gambaran seolah-olah CLR atau .NET Runtime merupakan "mini operating system" untuk aplikasi .NET. Gambaran tersebut tidak tepat.
CLR bukan operating system.
CLR tidak mengambil alih pengelolaan seluruh hardware dari Windows atau Linux. CLR menyediakan execution environment untuk managed code, sedangkan operating system tetap berada di bawahnya dan bertanggung jawab terhadap resource komputer secara keseluruhan.
Misalnya aplikasi membutuhkan memory. CLR memiliki mekanisme seperti garbage collector untuk mengelola memory managed object. Namun memory fisik komputer tetap dikelola oleh operating system.
Hubungannya dapat digambarkan seperti ini:
Plain Text
Physical Memory↓Operating System↓Process Memory↓.NET Runtime↓Managed Heap↓Managed Objects
CLR dapat mengelola object pada managed heap, tetapi CLR tidak mengelola RAM secara langsung pada level hardware. CLR meminta dan menggunakan memory melalui mekanisme yang disediakan operating system.
Hal yang sama berlaku untuk CPU.
.NET Runtime dapat mengatur bagaimana managed code dieksekusi, dan runtime dapat menggunakan thread serta mekanisme scheduling tertentu. Namun CPU scheduling pada level sistem tetap menjadi tanggung jawab operating system.
Secara sederhana:
Plain Text
CPU↓Operating System↓Process / Thread↓.NET Runtime↓Managed Code
Ketika JIT menghasilkan native machine code, CPU tidak mengetahui bahwa instruction tersebut berasal dari C# atau .NET. CPU hanya mengeksekusi instruction yang sesuai dengan instruction set processor.
Plain Text
C# Source Code↓C# Compiler↓IL↓.NET Runtime↓JIT↓Native Machine Code↓CPU
Karena itu, .NET Runtime dapat dipandang sebagai lapisan yang membuat managed application dapat berjalan di atas operating system, bukan sebagai pengganti operating system.
Hal yang sama juga berlaku untuk storage. Ketika aplikasi menyimpan file, .NET menyediakan API seperti:
C#
File.WriteAllText("data.txt", "Hello");
Kode tersebut terlihat sederhana. Namun di baliknya terdapat beberapa lapisan:
Plain Text
C# Application↓.NET API↓Operating System API↓Filesystem↓Storage Driver↓Storage Hardware
Aplikasi cukup menggunakan abstraksi yang diberikan .NET. Detail implementasi di bawahnya ditangani oleh runtime, operating system, driver, dan hardware.
Network memiliki pola yang serupa. ASP.NET Core dapat melakukan listening pada sebuah port tanpa perlu mengendalikan network card secara langsung.
Plain Text
ASP.NET Core↓.NET Networking APIs↓Operating System Networking Stack↓Network Driver↓Network Interface↓Network
Ketika sebuah HTTP request datang dari client, request tersebut melewati lapisan hardware dan operating system sebelum akhirnya sampai ke application.
Plain Text
Client││ HTTP Request▼Network↓Network Interface↓Operating System↓ASP.NET Core↓Application Code
Begitu pula ketika application mengirim response:
Plain Text
Application Code↓ASP.NET Core↓Operating System↓Network Interface↓Network↓Client
Dengan demikian, satu request HTTP sebenarnya melibatkan jauh lebih banyak komponen daripada sekadar ASP.NET Core.
Hal yang sama dapat dilihat dari sisi CPU. Ketika sebuah endpoint memiliki kode:
C#
app.MapGet("/hello", () => "Hello World");
CPU tidak menjalankan kode C# tersebut secara langsung.
Source code terlebih dahulu diproses oleh compiler:
Plain Text
C# Source Code↓Compiler↓IL + Metadata↓Assembly
Kemudian ketika aplikasi berjalan:
Plain Text
Assembly↓.NET Runtime↓JIT↓Native Machine Code↓CPU
Jadi, jika seluruh lapisan physical machine digabungkan, gambaran yang lebih lengkap menjadi:
Plain Text
┌───────────────────────────────┐│ ASP.NET Core App │├───────────────────────────────┤│ .NET Runtime ││ CLR / Runtime Services │├───────────────────────────────┤│ Operating System │├───────────────────────────────┤│ CPU │ Memory │ Storage │ NIC │├───────────────────────────────┤│ Physical Hardware │└───────────────────────────────┘
Setiap lapisan memiliki tanggung jawabnya masing-masing.
Application berisi business logic dan aturan aplikasi. .NET Runtime menyediakan environment untuk menjalankan managed code. Operating System mengelola process dan resource hardware serta menyediakan berbagai system services. Hardware menyediakan kemampuan fisik untuk melakukan computation, menyimpan data, dan berkomunikasi melalui network.
Pembagian tanggung jawab tersebut membuat aplikasi tidak perlu mengetahui detail hardware yang digunakan.
Misalnya sebuah aplikasi ASP.NET Core dapat dijalankan pada dua physical server dengan CPU yang berbeda:
Plain Text
Server ACPU A↓Windows↓.NET Runtime↓MyApp.dll
dan:
Plain Text
Server BCPU B↓Linux↓.NET Runtime↓MyApp.dll
Selama runtime dan environment yang diperlukan tersedia, application code dapat berjalan tanpa perlu ditulis ulang khusus untuk setiap hardware.
Hal tersebut merupakan salah satu manfaat penting dari abstraksi yang diberikan operating system dan .NET.
Application tidak perlu berpikir:
"Bagaimana cara menulis data langsung ke SSD?"
Application cukup mengatakan:
C#
File.WriteAllText(...);
Application juga tidak perlu berpikir:
"Bagaimana cara mengirim data langsung melalui network card?"
ASP.NET Core cukup menggunakan API networking yang tersedia.
Begitu pula application tidak perlu mengubah setiap instruksi C# menjadi machine code secara manual. Compiler dan runtime menangani proses tersebut.
Plain Text
Application↓.NET↓Operating System↓Hardware
Setiap lapisan menyederhanakan lapisan di bawahnya.
Konsep ini dapat disebut sebagai abstraction layer. Sebuah lapisan menggunakan kemampuan lapisan di bawahnya tanpa harus mengekspos seluruh detail implementasinya kepada lapisan di atas.
Contohnya:
Plain Text
ASP.NET Core↓menggunakan↓.NET Runtime↓menggunakan↓Operating System↓menggunakan↓Hardware
ASP.NET Core tidak perlu mengetahui detail bagaimana scheduler CPU bekerja. .NET Runtime juga tidak perlu mengendalikan CPU secara langsung. Operating system menangani bagian tersebut.
Begitu pula operating system tidak perlu mengetahui business logic aplikasi. Operating system hanya perlu menjalankan process dan menyediakan resource yang diminta process tersebut.
Pembagian seperti ini membuat sistem komputer dapat dibangun menjadi beberapa lapisan dengan tanggung jawab yang jelas.
Pada akhirnya, physical machine adalah fondasi paling bawah dari seluruh proses tersebut. Di atas hardware terdapat operating system, kemudian runtime, kemudian application.
Plain Text
Physical Machine↓Operating System↓.NET Runtime↓ASP.NET Core↓Application Code
Namun ketika dilihat dari sisi eksekusi CPU, perjalanan kode tetap kembali kepada pembahasan sebelumnya:
Plain Text
C# Source Code↓C# Compiler↓IL↓Assembly↓.NET Runtime↓JIT↓Native Machine Code↓CPU
Jadi, physical machine bukanlah sesuatu yang terpisah dari pembahasan tentang .NET Runtime. Justru physical machine adalah fondasi paling bawah tempat seluruh proses tersebut akhirnya terjadi.
.NET tidak menggantikan hardware. .NET juga tidak menggantikan operating system. .NET Runtime berada di atas operating system dan menyediakan environment yang membuat managed application dapat berjalan dengan memanfaatkan resource yang disediakan oleh operating system.
Jika seluruh hubungan tersebut dirangkum dalam satu diagram:
Plain Text
ASP.NET Core│▼.NET Runtime│┌────────┴────────┐│ │▼ ▼Managed RuntimeCode Services│▼Operating System│┌────────┼────────┐▼ ▼ ▼CPU Memory Storage│▼Network
Dengan pemahaman ini, posisi .NET menjadi lebih jelas. .NET bukanlah hardware, bukan operating system, dan bukan CPU. .NET adalah platform software yang berada di atas operating system dan menyediakan berbagai abstraction serta runtime services untuk menjalankan application.
Di bawah semuanya tetap terdapat physical machine.
Dan pada lapisan paling rendah, seluruh aplikasi tersebut pada akhirnya kembali kepada prinsip yang sangat sederhana:
Plain Text
Native Machine Instructions↓CPU
Inilah fondasi yang sama yang digunakan oleh hampir setiap aplikasi komputer, baik aplikasi .NET yang berjalan di laptop, server fisik, virtual machine, maupun environment cloud.
Bagian 19: Virtual Machine
Pada bagian sebelumnya, physical machine dibahas sebagai fondasi paling bawah tempat operating system dan aplikasi berjalan. Namun dalam banyak lingkungan modern, aplikasi tidak selalu dijalankan langsung di atas physical machine. Sering kali terdapat satu lapisan tambahan bernama Virtual Machine (VM).
Virtual machine memungkinkan sebuah physical machine menjalankan beberapa komputer virtual secara bersamaan. Setiap komputer virtual memiliki environment sendiri, termasuk virtual CPU, virtual memory, virtual storage, network interface, dan operating system.
Gambaran sederhananya seperti berikut:
Plain Text
Physical Machine│├── CPU├── Memory├── Storage└── Network│▼Hypervisor│┌────┴───────────────┐▼ ▼VM 1 VM 2│ │├── Guest OS ├── Guest OS├── .NET Runtime ├── .NET Runtime└── Application └── Application
Pada model ini, physical machine tidak langsung menjalankan ASP.NET Core application. Physical machine terlebih dahulu menjalankan hypervisor, kemudian hypervisor menyediakan virtual hardware untuk masing-masing VM.
Sebuah physical machine dengan spesifikasi tertentu, misalnya:
Plain Text
Physical Machine├── 32 CPU cores├── 128 GB RAM└── 2 TB Storage
dapat digunakan untuk menjalankan beberapa VM. Setiap VM mendapatkan sebagian resource tersebut dalam bentuk virtual.
Misalnya:
Plain Text
Physical Machine│├── Hypervisor│├── VM 1│ ├── 8 vCPU│ ├── 32 GB RAM│ └── Linux│├── VM 2│ ├── 8 vCPU│ ├── 32 GB RAM│ └── Linux│└── VM 3├── 4 vCPU├── 16 GB RAM└── Windows
Angka tersebut hanya contoh untuk menunjukkan konsep. Resource allocation sebenarnya bergantung pada konfigurasi virtualization platform dan workload yang dijalankan.
Apa Itu Hypervisor?
Hypervisor adalah software atau firmware layer yang bertugas membuat dan mengelola virtual machine. Hypervisor menyediakan abstraction terhadap physical hardware sehingga beberapa VM dapat menggunakan satu physical machine.
Secara sederhana:
Plain Text
Physical Hardware│▼Hypervisor│┌─────┼─────┐▼ ▼ ▼VM 1 VM 2 VM 3
Hypervisor bertanggung jawab untuk mengatur akses VM terhadap resource physical machine.
Misalnya VM membutuhkan CPU. VM melihat bahwa dirinya memiliki beberapa virtual CPU atau vCPU. Ketika virtual CPU tersebut perlu menjalankan instruction, hypervisor dan operating system host mengatur bagaimana kebutuhan tersebut mendapatkan waktu eksekusi pada physical CPU.
Begitu pula dengan memory. VM melihat seolah-olah memiliki memory sendiri:
Plain Text
VM└── 32 GB Virtual Memory
Padahal memory tersebut pada akhirnya berasal dari physical RAM:
Plain Text
Physical RAM│▼Hypervisor│├── VM 1 → Virtual Memory├── VM 2 → Virtual Memory└── VM 3 → Virtual Memory
Konsep ini memungkinkan beberapa VM berbagi satu physical machine tanpa harus masing-masing memiliki physical hardware sendiri.
Apakah VM Benar-Benar Sebuah Komputer?
Dari perspektif guest operating system, VM memang terlihat seperti sebuah komputer.
Misalnya sebuah VM menjalankan Linux. Linux dapat melihat:
Plain Text
CPUMemoryDiskNetwork Interface
Namun hardware tersebut adalah virtual hardware.
Plain Text
Physical Machine│▼Hypervisor│▼Virtual Hardware│▼Guest Operating System
Guest operating system tidak perlu mengetahui detail physical server yang berada di bawahnya. Guest OS cukup berinteraksi dengan virtual hardware yang diberikan oleh hypervisor.
Karena itulah sebuah VM dapat terasa seperti komputer lengkap.
Di dalam VM dapat dilakukan hal-hal seperti:
Bash
dotnet --info
kemudian aplikasi .NET dapat dijalankan:
Bash
dotnet MyApp.dll
Dari sudut pandang aplikasi .NET, environment tersebut terlihat seperti komputer biasa.
Perjalanan Aplikasi .NET di Dalam VM
Jika aplikasi ASP.NET Core berjalan di dalam VM, terdapat lebih banyak lapisan dibandingkan ketika aplikasi dijalankan langsung di physical machine.
Gambaran sederhananya:
Plain Text
Physical CPU│▼Hypervisor│▼Virtual CPU│▼Guest OS│▼.NET Runtime│▼ASP.NET Core│▼Application
Namun pada akhirnya CPU fisik tetap mengeksekusi native machine instructions.
Source code tetap melalui proses yang sama:
Plain Text
C# Source Code↓C# Compiler↓IL↓Assembly↓.NET Runtime↓JIT↓Native Machine Code↓Virtual CPU↓Physical CPU
Lapisan virtualisasi tidak mengubah konsep dasar compilation dan runtime .NET. Virtualisasi hanya menambahkan lapisan abstraction antara operating system guest dengan physical hardware.
Apakah Ada CPU Virtual?
Ya. VM biasanya diberikan virtual CPU, sering disebut vCPU.
Misalnya physical machine memiliki:
Plain Text
32 physical CPU cores
Hypervisor dapat memberikan kepada sebuah VM:
Plain Text
8 vCPU
Guest operating system kemudian melihat seolah-olah komputer tersebut memiliki 8 CPU yang dapat digunakan.
Plain Text
Physical CPU│▼Hypervisor│▼8 vCPU│▼Guest OS│▼Application
Namun vCPU bukan CPU fisik baru.
vCPU merupakan abstraction yang memungkinkan guest OS menggunakan computational resource yang disediakan oleh physical infrastructure.
Ketika application menjalankan native instruction melalui vCPU, instruction tersebut pada akhirnya perlu dieksekusi oleh physical CPU.
Secara konseptual:
Plain Text
Native Instruction↓vCPU↓Hypervisor↓Physical CPU
Detail implementasinya bergantung pada teknologi virtualization dan hardware processor yang digunakan. Modern CPU menyediakan berbagai hardware virtualization capabilities untuk membuat proses ini lebih efisien.
Apakah VM Membuat CPU Menjadi Lambat?
Virtualization memang menambahkan abstraction layer, tetapi modern virtualization hardware membuat overhead tersebut dapat dibuat sangat kecil untuk banyak workload.
CPU modern memiliki dukungan virtualization yang memungkinkan hypervisor mengelola VM dengan lebih efisien dibandingkan pendekatan software virtualization murni.
Yang penting untuk dipahami adalah bahwa VM tidak berarti setiap instruction harus selalu melewati proses yang sangat berat:
Plain Text
Instruction↓Virtualization↓Emulation↓Physical CPU
Virtualization modern tidak sekadar melakukan emulasi CPU secara penuh. Banyak instruction dapat dieksekusi dengan sangat dekat dengan performa native, sementara hypervisor menangani bagian-bagian yang membutuhkan virtualization atau isolation.
Karena itu VM dapat digunakan untuk menjalankan workload production yang serius.
Guest OS dan Host
Dalam virtualisasi terdapat istilah host dan guest.
Physical machine yang menyediakan hardware disebut sebagai host.
Operating system yang berjalan di dalam VM disebut guest operating system.
Gambaran sederhananya:
Plain Text
Host│├── Physical Hardware│└── Hypervisor│├── Guest VM 1│ └── Guest OS│└── Guest VM 2└── Guest OS
Istilah ini membantu membedakan dua environment.
Misalnya:
Plain Text
Host Machine↓Hypervisor↓VM↓Ubuntu↓.NET Runtime↓ASP.NET Core
Ubuntu tersebut merupakan guest OS dari VM, sedangkan physical machine berada di sisi host.
Namun perlu diperhatikan bahwa arsitektur hypervisor dapat berbeda. Ada hypervisor yang berjalan sangat dekat dengan hardware dan ada pula model virtualisasi yang menggunakan operating system host sebagai bagian dari environment virtualization.
Apakah CLR Adalah Virtual Machine?
Di titik ini muncul kebingungan yang cukup umum.
CLR kadang disebut sebagai virtual execution environment, dan dalam literatur lama atau konteks tertentu istilah virtual machine juga pernah digunakan untuk menggambarkan CLR. Namun CLR bukan virtual machine dalam pengertian infrastructure virtualization seperti VMware, VirtualBox, atau Hyper-V.
Perbedaan utamanya terletak pada apa yang divirtualisasikan.
Virtual machine infrastructure membuat sebuah komputer virtual:
Plain Text
VM├── Virtual CPU├── Virtual Memory├── Virtual Storage├── Virtual Network└── Guest Operating System
Sedangkan CLR menyediakan environment untuk menjalankan managed .NET code:
Plain Text
CLR├── Managed execution├── Garbage Collection├── Type system├── Exception handling├── JIT└── Runtime services
CLR tidak membuat sebuah komputer virtual lengkap yang memiliki guest operating system sendiri.
Misalnya ketika menjalankan:
Bash
dotnet MyApp.dll
CLR tidak membuat:
Plain Text
Virtual CPUVirtual RAMVirtual DiskVirtual NetworkGuest Windows/Linux
CLR menjalankan managed application di dalam operating system yang sudah ada.
Jadi jika aplikasi .NET berjalan di dalam VM, urutannya dapat menjadi:
Plain Text
Physical Machine↓Hypervisor↓Virtual Machine↓Guest OS↓.NET Runtime / CLR↓ASP.NET Core↓Application
Di sini terlihat jelas bahwa VM dan CLR berada pada lapisan yang berbeda.
VM vs CLR
Perbedaan tersebut dapat diringkas sebagai berikut:
| Konsep | Fungsi |
|---|---|
| Physical Machine | Menyediakan hardware sebenarnya |
| Hypervisor | Membuat dan mengelola virtual machine |
| Virtual Machine | Menyediakan komputer virtual |
| Guest OS | Operating system yang berjalan di dalam VM |
| .NET Runtime / CLR | Menyediakan execution environment untuk managed .NET code |
| JIT | Mengubah IL menjadi native machine code |
| CPU | Mengeksekusi machine instructions |
Masing-masing lapisan memiliki tanggung jawab yang berbeda.
Plain Text
Physical Machine│▼Hypervisor│▼Virtual Machine│▼Guest OS│▼.NET Runtime / CLR│▼JIT│▼Native Machine Code│▼Physical CPU
Dengan struktur tersebut, CLR tidak menggantikan hypervisor, VM, atau operating system.
Mengapa Virtual Machine Dibutuhkan?
Virtual machine memberikan beberapa manfaat penting dalam infrastructure.
Salah satunya adalah isolation. Beberapa application environment dapat berjalan pada VM yang berbeda.
Misalnya:
Plain Text
Physical Server│├── VM 1│ └── Production Application│├── VM 2│ └── Database│└── VM 3└── Development Environment
Masing-masing VM memiliki environment sendiri sehingga software di dalam satu VM tidak secara langsung bercampur dengan filesystem dan process VM lainnya.
VM juga memungkinkan resource physical machine digunakan dengan lebih efisien. Daripada menyediakan satu physical server untuk setiap workload, beberapa workload dapat ditempatkan pada VM berbeda yang berjalan di satu physical server.
Plain Text
Tanpa VirtualizationServer 1 → Application AServer 2 → Application BServer 3 → Application C
Dengan virtualization:
Plain Text
One Physical Server│├── VM 1 → Application A├── VM 2 → Application B└── VM 3 → Application C
Tentu saja desain sebenarnya harus mempertimbangkan CPU, memory, storage, network, availability, security, dan workload. Virtualisasi bukan berarti semua workload selalu harus ditempatkan dalam satu physical server.
VM dalam Cloud
Konsep virtual machine juga menjadi salah satu fondasi penting cloud computing.
Ketika sebuah cloud provider menyediakan sebuah virtual machine, pengguna biasanya tidak melihat physical server yang sebenarnya.
Yang terlihat hanyalah:
Plain Text
Cloud VM├── vCPU├── RAM├── Disk├── Network└── Operating System
Di bawahnya dapat terdapat:
Plain Text
Physical Data Center↓Physical Servers↓Hypervisor↓Virtual Machines
Detail tersebut biasanya dikelola oleh cloud provider.
Dari sudut pandang aplikasi .NET, VM cloud tersebut tetap terlihat seperti komputer yang menjalankan operating system.
Misalnya:
Plain Text
Cloud VM↓Linux↓.NET Runtime↓MyApp.dll
Aplikasi tidak perlu mengetahui physical server mana yang menjalankan VM tersebut.
Virtual Machine Tidak Mengubah Cara .NET Bekerja
Salah satu konsep penting dari bagian ini adalah bahwa virtualisasi tidak mengubah fundamental execution model .NET.
Di komputer fisik:
Plain Text
C#↓Compiler↓IL↓.NET Runtime↓JIT↓Native Code↓CPU
Di dalam VM:
Plain Text
C#↓Compiler↓IL↓.NET Runtime↓JIT↓Native Code↓vCPU↓Physical CPU
Perbedaannya hanya terdapat pada tambahan lapisan virtualization.
CLR tetap melakukan pekerjaan yang sama. JIT tetap menghasilkan native code. Guest OS tetap mengelola process dan memory dalam environment-nya. Hypervisor kemudian mengelola hubungan antara virtual hardware dengan physical hardware.
Dengan kata lain, aplikasi .NET tidak perlu mengetahui apakah dirinya berjalan langsung di physical machine atau di dalam VM.
Bagian 20: Container
Setelah memahami virtual machine, ada bentuk deployment lain yang saat ini sangat umum digunakan, yaitu container. Container digunakan untuk menjalankan aplikasi dalam sebuah lingkungan yang terisolasi, sehingga aplikasi dapat membawa dependency dan environment yang dibutuhkannya tanpa harus bergantung terlalu banyak pada konfigurasi mesin tempat aplikasi tersebut dijalankan.
Secara sederhana, posisi container dalam sebuah sistem dapat digambarkan seperti berikut:
Plain Text
Physical / Cloud Infrastructure↓Operating System↓Container Runtime↓Container↓.NET Runtime↓ASP.NET Core App
Gambaran tersebut menunjukkan bahwa container berada di antara operating system dan aplikasi. Di dalam container dapat terdapat .NET Runtime beserta aplikasi ASP.NET Core yang akan dijalankan.
Container Bukan Virtual Machine
Meskipun container dan virtual machine sama-sama memberikan lingkungan yang terisolasi untuk menjalankan aplikasi, keduanya menggunakan pendekatan yang berbeda.
Virtual machine melakukan virtualisasi sebuah komputer. Di atas physical machine terdapat hypervisor yang menyediakan hardware virtual untuk setiap virtual machine. Setiap VM kemudian memiliki guest operating system sendiri. Di atas operating system tersebut barulah aplikasi dan runtime dijalankan.
Plain Text
Physical Hardware↓Hypervisor↓Virtual Machine↓Guest OS↓.NET Runtime↓Application
Sebagai contoh, sebuah physical server dapat menjalankan beberapa virtual machine. Setiap virtual machine dapat menggunakan Linux atau Windows sebagai guest operating system masing-masing. Artinya, setiap VM membawa operating system sendiri.
Container tidak bekerja seperti itu. Container tidak menjalankan guest operating system lengkap untuk setiap aplikasi. Container menggunakan kemampuan yang sudah disediakan oleh operating system host untuk mengisolasi process, filesystem, network, dan resource aplikasi.
Plain Text
Physical Hardware↓Operating System↓Container Runtime↓Container↓.NET Runtime↓Application
Inilah perbedaan paling penting antara VM dan container. VM melakukan virtualisasi pada level komputer, sedangkan container melakukan isolasi terutama pada level process dan operating system.
Mengapa Container Lebih Ringan?
Container umumnya lebih ringan dibandingkan virtual machine karena tidak perlu menjalankan operating system lengkap untuk setiap instance.
Pada VM, sebuah aplikasi membutuhkan guest operating system sebelum aplikasi tersebut dapat berjalan:
Plain Text
VM│├── Guest OS│ ├── Kernel│ ├── System Libraries│ └── System Services│├── .NET Runtime└── Application
Jika terdapat beberapa VM, masing-masing VM membawa guest operating system sendiri. Akibatnya, terdapat resource tambahan yang digunakan untuk menjalankan operating system tersebut.
Container menggunakan pendekatan yang berbeda:
Plain Text
Host OS│├── Container 1│ ├── .NET Runtime│ └── Application│├── Container 2│ ├── .NET Runtime│ └── Application│└── Container 3├── .NET Runtime└── Application
Ketiga container tersebut menggunakan kernel dari operating system host yang sama. Tidak ada kernel operating system terpisah yang harus dijalankan untuk setiap container.
Karena itu, container umumnya dapat dibuat, dijalankan, dan dihentikan dengan lebih cepat serta membutuhkan resource yang lebih kecil dibandingkan VM. Inilah salah satu alasan container sangat populer untuk deployment aplikasi modern.
Peran Container Runtime
Container tetap membutuhkan sebuah komponen yang bertugas membuat dan menjalankan container. Komponen tersebut disebut container runtime.
Container runtime bertanggung jawab terhadap lifecycle container, seperti membuat container, menjalankan process di dalamnya, menghentikan container, mengatur filesystem, network, serta membantu menerapkan batasan resource.
Docker merupakan salah satu teknologi yang paling populer dalam ekosistem container. Docker menyediakan berbagai tools dan interface yang memudahkan proses membuat image, menjalankan container, mengelola network, dan melakukan deployment.
Di balik teknologi seperti Docker terdapat komponen lain seperti containerd dan runc yang menangani bagian tertentu dari proses menjalankan container. Detail tersebut tidak perlu dipahami terlebih dahulu untuk memahami konsep dasar container. Hal yang paling penting adalah memahami bahwa container runtime merupakan komponen yang mengatur bagaimana container dibuat dan dijalankan di atas operating system.
Container Image
Sebelum sebuah container dijalankan, biasanya terdapat sebuah container image. Image dapat dipahami sebagai template atau blueprint yang digunakan untuk membuat container.
Misalnya terdapat sebuah image bernama myapp. Image tersebut dapat berisi aplikasi ASP.NET Core, .NET Runtime, dependency, serta file-file lain yang diperlukan aplikasi.
Plain Text
Container Image↓Container↓Running Application
Satu image dapat digunakan untuk membuat beberapa container.
Plain Text
Container Image│┌─────────┼─────────┐↓ ↓ ↓Container 1 Container 2 Container 3
Image bersifat seperti template, sedangkan container merupakan instance yang dibuat berdasarkan template tersebut. Karena itu, image dan container bukanlah hal yang sama.
Analogi sederhananya adalah blueprint sebuah rumah dan rumah yang dibangun berdasarkan blueprint tersebut. Blueprint menjelaskan bentuk yang akan dibuat, sedangkan rumah merupakan hasil nyata dari blueprint tersebut.
Apa yang Ada di Dalam Container?
Sebuah container dapat menyediakan filesystem dan environment yang dibutuhkan oleh aplikasi. Untuk aplikasi ASP.NET Core, secara sederhana isi environment tersebut dapat dibayangkan seperti berikut:
Plain Text
Container│├── .NET Runtime│├── Application Files│ ├── MyApp.dll│ ├── MyApp.deps.json│ └── MyApp.runtimeconfig.json│└── Dependencies
Ketika container dijalankan, .NET Runtime di dalam environment tersebut digunakan untuk menjalankan aplikasi.
Plain Text
Container↓.NET Runtime↓MyApp.dll↓ASP.NET Core Application
Hal ini sebenarnya tidak mengubah cara dasar aplikasi .NET dijalankan. MyApp.dll tetap membutuhkan .NET Runtime. Perbedaannya adalah runtime dan aplikasi tersebut sekarang dijalankan di dalam environment container yang terisolasi.
Container Tetap Membutuhkan Operating System
Container bukan operating system dan bukan pengganti operating system. Container tetap berjalan di atas operating system.
Struktur sederhananya adalah:
Plain Text
Hardware↓Operating System↓Container Runtime↓Container↓Application
Hal yang membedakannya dengan VM adalah container tidak membawa operating system lengkap sendiri.
Pada VM:
Plain Text
Physical Machine↓Hypervisor↓Virtual Machine↓Guest OS↓Application
Sedangkan pada container:
Plain Text
Physical Machine↓Operating System↓Container Runtime↓Container↓Application
Dengan kata lain, VM dapat dianggap sebagai komputer virtual, sedangkan container lebih tepat dipahami sebagai environment terisolasi untuk menjalankan process.
Bagaimana Container Bisa Terisolasi?
Meskipun beberapa container berjalan pada operating system host yang sama, process di dalam sebuah container tidak begitu saja dapat mengakses seluruh resource yang ada di host.
Operating system menyediakan mekanisme untuk melakukan isolasi tersebut. Pada Linux, salah satu mekanisme penting yang digunakan adalah namespaces.
Namespaces membuat sebuah process memiliki pandangan tertentu terhadap resource sistem. Sebagai contoh, process yang berjalan di dalam container dapat melihat filesystem, network interface, hostname, dan process tertentu yang berbeda dari pandangan process di host.
Secara sederhana, dapat dibayangkan seperti beberapa ruangan yang berada di dalam satu gedung. Semua ruangan menggunakan gedung dan fondasi yang sama, tetapi isi sebuah ruangan tidak secara otomatis dapat melihat seluruh isi ruangan lainnya.
Selain namespaces, Linux juga memiliki mekanisme bernama control groups atau cgroups. Jika namespaces digunakan untuk membantu mengisolasi apa yang dapat dilihat oleh process, cgroups digunakan untuk mengatur dan membatasi resource yang dapat digunakan oleh process tersebut.
Sebagai contoh, sebuah container dapat diberikan batas penggunaan memory sebesar 512 MB atau dibatasi agar hanya menggunakan sejumlah CPU tertentu.
Plain Text
Container A├── CPU → batas tertentu└── Memory → 512 MBContainer B├── CPU → batas tertentu└── Memory → 1 GB
Dengan mekanisme tersebut, sebuah aplikasi yang berjalan di dalam container tidak secara bebas menggunakan seluruh resource server.
Mengapa Container Berguna untuk Deployment?
Salah satu masalah dalam deployment aplikasi adalah perbedaan environment antara komputer developer, server testing, dan server production.
Sebagai contoh, sebuah aplikasi mungkin berjalan pada komputer developer karena memiliki .NET Runtime versi tertentu dan dependency tertentu. Ketika aplikasi tersebut dipindahkan ke server, versi runtime atau dependency yang tersedia mungkin berbeda sehingga aplikasi tidak berjalan dengan cara yang sama.
Container membantu mengurangi masalah tersebut dengan menyediakan environment aplikasi dalam bentuk image.
Misalnya sebuah image berisi:
Plain Text
Application+.NET Runtime+Dependencies+Required Files
Image tersebut kemudian dapat digunakan pada beberapa environment:
Plain Text
Development↓Container Image↓Testing↓Container Image↓Production
Dengan pendekatan ini, image yang sama dapat digunakan dari tahap testing sampai production. Environment aplikasi menjadi lebih konsisten karena aplikasi tidak hanya dikirim sebagai sekumpulan file, tetapi bersama environment yang dibutuhkan untuk menjalankannya.
Hal ini tidak berarti seluruh konfigurasi production harus dimasukkan ke dalam image. Informasi seperti connection string, password, API key, dan konfigurasi environment-specific tetap sebaiknya diberikan melalui mekanisme konfigurasi dan secret management yang sesuai.
Container dan Virtual Machine Dapat Digunakan Bersama
Container dan virtual machine bukan dua teknologi yang harus selalu dipilih salah satu. Keduanya dapat digunakan secara bersamaan.
Contohnya, sebuah server cloud dapat menyediakan sebuah virtual machine. Di dalam VM tersebut terdapat Linux sebagai operating system. Linux kemudian menjalankan container runtime, dan container runtime menjalankan beberapa container.
Plain Text
Physical / Cloud Infrastructure↓Virtual Machine↓Linux OS↓Container Runtime↓┌──────┴──────┐↓ ↓Container 1 Container 2↓ ↓ASP.NET Core ASP.NET Core
Dalam skenario tersebut, VM memberikan isolasi pada level virtual machine, sedangkan container memberikan isolasi pada level aplikasi dan process.
Pendekatan seperti ini sangat umum dalam infrastruktur cloud karena masing-masing teknologi memiliki tujuan yang berbeda.
Hubungannya dengan .NET Runtime
Jika pembahasan mengenai compiler, assembly, .NET Runtime, dan ASP.NET Core sebelumnya disatukan dengan container, alurnya menjadi lebih jelas.
Aplikasi .NET pada akhirnya menghasilkan assembly seperti MyApp.dll. Assembly tersebut membutuhkan .NET Runtime untuk dijalankan. Ketika aplikasi ditempatkan ke dalam container, .NET Runtime dan aplikasi tersebut berada di dalam environment container.
Plain Text
Physical / Cloud Infrastructure↓Operating System↓Container Runtime↓Container↓.NET Runtime↓ASP.NET Core App↓MyApp.dll
Setiap lapisan memiliki tanggung jawab yang berbeda.
Operating system menyediakan kemampuan dasar seperti process, memory, filesystem, dan network. Container runtime mengelola container dan isolasinya. Container menyediakan environment terisolasi untuk aplikasi. .NET Runtime menyediakan environment untuk mengeksekusi aplikasi .NET. Sementara ASP.NET Core menyediakan framework yang digunakan untuk membangun aplikasi web.
Dengan demikian, container tidak menggantikan .NET Runtime. Container juga tidak menggantikan operating system. Container hanya menjadi lapisan tambahan yang menyediakan environment terisolasi untuk menjalankan aplikasi.
Bagian 21: Apa yang Terjadi di Azure?
Setelah physical machine, virtual machine, operating system, dan container dibahas satu per satu, pembahasan berikutnya dapat diarahkan ke lingkungan yang lebih nyata, yaitu cloud. Salah satu contoh cloud platform yang banyak digunakan untuk menjalankan aplikasi .NET adalah Microsoft Azure.
Sampai titik ini, sebuah aplikasi .NET dapat dibayangkan berjalan di atas sebuah physical machine. Physical machine menyediakan CPU, memory, storage, dan network. Operating system kemudian menggunakan hardware tersebut untuk menyediakan lingkungan bagi process. Di atas operating system, .NET Runtime menjalankan aplikasi .NET.
Dalam cloud, konsep tersebut sebenarnya tidak berubah. Yang berubah adalah siapa yang menyediakan dan mengelola physical infrastructure tersebut.
Pada server milik sendiri, sebuah perusahaan mungkin harus menyediakan server fisik, memasang operating system, melakukan konfigurasi network, menyediakan storage, melakukan maintenance hardware, dan menangani berbagai kebutuhan infrastruktur lainnya. Pada cloud seperti Azure, sebagian besar pekerjaan tersebut dikelola oleh cloud provider.
Secara sederhana, Azure dapat dipandang sebagai lapisan infrastruktur yang menyediakan berbagai resource komputasi di atas physical datacenter milik Microsoft.
Plain Text
Microsoft Datacenter↓Physical Infrastructure↓Azure Infrastructure↓Compute Resource↓Operating System / Runtime↓Application
Developer biasanya tidak berinteraksi langsung dengan physical server yang menjalankan aplikasi. Sebagai gantinya, Azure menyediakan abstraction berupa resource seperti Virtual Machine, App Service, Container Apps, dan berbagai layanan compute lainnya.
Jika Menggunakan Virtual Machine
Salah satu cara paling sederhana untuk menjalankan aplikasi di Azure adalah menggunakan Virtual Machine. Dalam skenario ini, Azure menyediakan sebuah VM yang secara konseptual dapat dipandang sebagai sebuah komputer virtual.
Plain Text
Azure Datacenter↓Azure Infrastructure↓Virtual Machine↓Operating System↓.NET Runtime↓ASP.NET Core App↓CPU
Misalnya sebuah VM menggunakan Linux sebagai operating system. Setelah VM tersedia, .NET Runtime dapat dipasang di dalamnya dan aplikasi ASP.NET Core dapat dijalankan seperti pada server biasa.
Dari sudut pandang aplikasi, tidak terlalu banyak perbedaan antara menjalankan aplikasi pada server fisik dan menjalankannya pada VM Azure. Keduanya tetap menyediakan operating system yang kemudian menjalankan .NET Runtime.
Perbedaannya adalah physical hardware yang digunakan berada di datacenter Azure dan virtual machine tersebut dikelola melalui platform Azure.
Pada pendekatan ini, developer memiliki kontrol yang cukup besar. Operating system dapat dikonfigurasi, software dapat dipasang, service dapat dijalankan, firewall dapat diatur, dan berbagai pengaturan server lainnya dapat dikendalikan secara langsung.
Namun kontrol tersebut juga berarti lebih banyak tanggung jawab. Operating system perlu di-update, security patch perlu diperhatikan, software perlu dikonfigurasi, monitoring perlu disiapkan, dan kapasitas VM perlu disesuaikan dengan kebutuhan aplikasi.
Dengan kata lain, VM di Azure memberikan kontrol yang lebih besar, tetapi sebagian pekerjaan administrasi server tetap menjadi tanggung jawab pihak yang menggunakan VM tersebut.
Jika Menggunakan Container
Pendekatan lain adalah menjalankan aplikasi dalam container. Dalam skenario ini, aplikasi terlebih dahulu dikemas menjadi container image. Image tersebut berisi aplikasi dan environment yang dibutuhkan untuk menjalankannya.
Secara sederhana:
Plain Text
Container Image↓.NET Runtime↓ASP.NET Core App
Image tersebut kemudian dijalankan menggunakan layanan container di Azure.
Secara konseptual, alurnya dapat digambarkan seperti berikut:
Plain Text
Azure Infrastructure↓Container Platform↓Container↓.NET Runtime↓ASP.NET Core App↓CPU
Hal penting yang perlu dipahami adalah bahwa developer tidak selalu harus mengetahui VM yang secara fisik menjalankan container tersebut.
Pada layanan container yang dikelola Azure, platform menangani sebagian pekerjaan infrastructure seperti menyediakan compute resource, menjalankan container, melakukan scheduling, dan menangani aspek operasional tertentu sesuai layanan yang digunakan.
Dengan demikian, fokus dapat lebih banyak diarahkan pada container image dan aplikasi.
Contohnya, sebuah aplikasi ASP.NET Core dapat dikemas menjadi:
Plain Text
myapp:1.0
Image tersebut kemudian dapat dijalankan pada environment development, testing, maupun production.
Plain Text
myapp:1.0↓┌─────────┼─────────┐↓ ↓ ↓Development Testing Production
Pendekatan ini membantu menjaga agar environment aplikasi lebih konsisten karena artifact yang digunakan dapat berasal dari image yang sama.
Jika Menggunakan Azure App Service
Azure juga menyediakan layanan yang lebih terkelola, salah satunya adalah Azure App Service.
Pada pendekatan ini, developer tidak perlu membuat sebuah VM secara manual kemudian memasang operating system, .NET Runtime, web server, dan melakukan konfigurasi deployment satu per satu.
Sebagian besar detail tersebut dikelola oleh Azure.
Secara konseptual:
Plain Text
Azure Infrastructure↓Azure App Service↓.NET Runtime / Hosting Environment↓ASP.NET Core Application
Dari sisi developer, perhatian dapat lebih banyak diarahkan pada aplikasi dan konfigurasi deployment.
Tidak perlu mengetahui physical server mana yang menjalankan aplikasi. Tidak perlu melakukan provisioning physical hardware. Bahkan dalam banyak skenario, tidak perlu mengelola operating system secara langsung.
Inilah salah satu karakteristik penting dari managed service. Platform mengambil alih sebagian pekerjaan infrastructure sehingga developer dapat bekerja pada level abstraction yang lebih tinggi.
Namun, "managed" bukan berarti seluruh infrastruktur menghilang. Infrastruktur tersebut tetap ada. CPU tetap diperlukan. Memory tetap diperlukan. Storage dan network tetap digunakan. Operating system dan proses hosting tetap berjalan di balik layanan tersebut. Hanya saja, detail tersebut disembunyikan dan dikelola oleh platform.
Apa yang Sebenarnya Terjadi di Balik App Service?
Dari luar, App Service dapat terlihat sangat sederhana:
Plain Text
Application↓Azure App Service
Namun di balik abstraction tersebut tetap terdapat infrastruktur komputasi.
Secara konseptual, gambaran yang lebih lengkap dapat dibuat seperti:
Plain Text
Azure Datacenter↓Physical Infrastructure↓Azure Compute Infrastructure↓Managed Hosting Environment↓.NET Runtime↓ASP.NET Core Application↓CPU
Detail implementasi sebenarnya jauh lebih kompleks daripada diagram tersebut. Azure dapat menggunakan berbagai lapisan virtualization, networking, storage, orchestration, dan platform management yang tidak perlu diketahui oleh aplikasi.
Hal pentingnya adalah bahwa abstraction tidak berarti hardware tidak ada. Ketika sebuah aplikasi dijalankan di Azure App Service, CPU tetap bekerja. Memory tetap digunakan. Operating system tetap ada. Process tetap dijalankan. Perbedaannya adalah sebagian besar detail tersebut tidak perlu dikelola secara manual oleh developer.
Managed Service Mengubah Tanggung Jawab
Perbedaan utama antara VM dan managed service sebenarnya dapat dilihat dari pembagian tanggung jawab. Pada VM, pihak yang menggunakan VM memiliki tanggung jawab lebih besar terhadap operating system.
Plain Text
Azure↓VM↓Operating System↓.NET Runtime↓Application
Operating system berada cukup dekat dengan pihak yang mengelola aplikasi.
Pada managed service:
Plain Text
Azure↓Managed Service↓Hosting Environment↓.NET Runtime↓Application
Sebagian lapisan di antara infrastructure dan application dikelola oleh Azure.
Semakin tinggi abstraction yang digunakan, semakin sedikit detail infrastructure yang perlu dikelola secara langsung. Sebagai konsekuensinya, kontrol terhadap infrastructure juga biasanya menjadi lebih terbatas. Hal ini merupakan salah satu trade-off utama dalam cloud computing.
VM memberikan kontrol yang lebih besar tetapi membutuhkan lebih banyak pengelolaan. Managed service mengurangi pekerjaan infrastructure tetapi memberikan kontrol yang lebih terbatas terhadap detail di bawahnya.
Lalu Di Mana CPU Berada?
Pada pembahasan sebelumnya, CPU berada di bagian paling bawah karena pada akhirnya seluruh aplikasi harus dieksekusi oleh CPU. Hal tersebut tetap berlaku di Azure.
Misalnya menggunakan VM:
Plain Text
ASP.NET Core App↓.NET Runtime↓Operating System↓Virtual Machine↓Azure Infrastructure↓Physical CPU
Jika menggunakan container:
Plain Text
ASP.NET Core App↓.NET Runtime↓Container↓Container Runtime↓Operating System↓Azure Infrastructure↓Physical CPU
Jika menggunakan managed service seperti App Service, sebagian besar lapisan tersebut tidak terlihat secara langsung:
Plain Text
ASP.NET Core App↓Azure App Service↓Azure Infrastructure↓Physical CPU
Diagram terakhir bukan berarti .NET Runtime, operating system, process, atau layer infrastructure lainnya benar-benar tidak ada. Diagram tersebut hanya menunjukkan bahwa detail tersebut diabstraksikan oleh Azure. Pada akhirnya, instruction yang harus dieksekusi tetap berakhir pada CPU.
Bagaimana dengan IL dan JIT?
Hal ini menghubungkan kembali pembahasan Azure dengan konsep .NET Runtime yang sudah dibahas sebelumnya.
Kode C# tidak langsung menjadi native machine code ketika source code ditulis. Source code terlebih dahulu dikompilasi menjadi IL dan disimpan di dalam assembly seperti MyApp.dll.
Ketika aplikasi dijalankan, .NET Runtime membaca IL tersebut. Bagian tertentu dari IL kemudian dikompilasi oleh JIT menjadi native code yang sesuai dengan arsitektur CPU tempat aplikasi berjalan.
Secara konseptual:
Plain Text
C# Source Code↓C# Compiler↓IL↓MyApp.dll↓.NET Runtime↓JIT↓Native Machine Code↓CPU
Deployment ke Azure tidak mengubah konsep tersebut.
Jika aplikasi ASP.NET Core berjalan pada Azure VM, Azure App Service, atau container yang berjalan di Azure, aplikasi tetap membutuhkan .NET Runtime untuk menjalankan assembly-nya.
Pada saat runtime, JIT tetap menghasilkan native code yang kemudian dieksekusi oleh CPU. Dengan demikian, cloud tidak menggantikan mekanisme dasar .NET. Cloud hanya menyediakan infrastructure dan berbagai abstraction untuk menjalankan aplikasi tersebut.
Jadi, Apa yang Sebenarnya Berubah Ketika Berpindah ke Azure?
Jika dibandingkan dengan menjalankan aplikasi langsung pada physical server, perubahan terbesar ketika menggunakan Azure bukan berada pada cara C# dieksekusi.
C# tetap dikompilasi.
IL tetap berada di dalam assembly.
.NET Runtime tetap menjalankan assembly.
JIT tetap menghasilkan native code.
CPU tetap mengeksekusi native code.
Yang berubah terutama adalah cara infrastructure disediakan dan dikelola.
Pada server fisik sendiri:
Plain Text
Physical Server↓Operating System↓.NET Runtime↓Application
Pada VM Azure:
Plain Text
Azure↓Virtual Machine↓Operating System↓.NET Runtime↓Application
Pada container:
Plain Text
Azure↓Container Platform↓Container↓.NET Runtime↓Application
Pada managed service:
Plain Text
Azure↓Managed Service↓.NET Runtime / Hosting Environment↓Application
Lapisan infrastructure yang terlihat oleh developer berubah, tetapi lapisan fundamental .NET tetap sama.
Bagian 22: Ringkasan
Ringkasan ini tidak dimaksudkan untuk mengulang semua definisi yang sudah dibahas. Yang lebih penting adalah perubahan cara pandang yang seharusnya terjadi setelah membaca seluruh perjalanan ini.
Ketika sebuah aplikasi .NET dibuat, source code bukanlah sesuatu yang langsung dijalankan CPU. Ada beberapa lapisan yang dilalui sebelum sampai ke sana:
Plain Text
Source Code↓Compiler↓IL↓Assembly↓.NET Runtime / CLR↓JIT↓Native Code↓CPU
Dan menariknya, source code tersebut tidak harus berasal dari C#.
Plain Text
C# ──────┐VB.NET ──┼──→ Compiler → IL → .NET Runtime → CPUF# ──────┘
Inilah salah satu ide penting di balik desain .NET: bahasa pemrograman dapat berbeda, tetapi tetap dapat berbagi execution environment dan library ecosystem yang sama.
Dengan memahami alur tersebut, istilah-istilah seperti C#, VB.NET, F#, compiler, IL, assembly, metadata, CTS, CLS, CLR, .NET Runtime, JIT, GC, machine code, CPU, VM, container, server, hingga Azure tidak lagi terlihat sebagai konsep yang berdiri sendiri-sendiri. Semuanya merupakan bagian dari perjalanan yang sama, yaitu mengubah kode yang ditulis manusia menjadi instruksi yang akhirnya dapat dijalankan oleh CPU.
Daftar isi
Baca juga
Artikel sebelumnya

17 Agustus 2026
•
12 menit baca
Memahami Reflection di C#: Cara Kerja, Penggunaan, dan Contoh Implementasi
Reflection memungkinkan program .NET mengenali dan berinteraksi dengan struktur kode saat aplikasi sedang berjalan. Mulai dari membaca property, field, method, constructor, dan Attribute hingga membuat object, memanggil method, dan memindai assembly, semuanya dapat dilakukan tanpa harus mengetahui struktur tipe tersebut sejak awal. Artikel ini membahas Reflection secara bertahap melalui contoh sederhana hingga beberapa studi kasus seperti serializer, Dependency Injection, object mapper, dan plugin system.
Artikel lain

14 September 2022
•
8 menit baca
Abstract Class dan Interface Apa Perbedaannya? (C#)
Pembahasan ringan tentang kapan menggunakan abstract class, kapan lebih cocok memakai interface, dan bagaimana keduanya membantu desain OOP menjadi lebih jelas.