KembaliKembali ke artikel

.NET di Balik Layar

24 Agustus 2026

2 jam baca

2 pembaca

.NET di Balik Layar

Memahami bagaimana aplikasi .NET berjalan di Azure, mulai dari VM, container, hingga managed service.

C#Advanced C#

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

Console
WriteLine
"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:

  • Console merupakan sebuah type yang tersedia dalam library .NET.

  • WriteLine merupakan 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 : Windows
CPU : x86-64

atau:

Plain Text

Operating System : Linux
CPU : 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;
↓ Compiler
IL:
instruksi .NET untuk
memuat nilai,
melakukan operasi,
dan menyimpan hasil
↓ Runtime / JIT
Machine 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 ───────────┼──► IL
F# ───────────┘
.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
IL
VB.NET Source
VB Compiler
IL
F# 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 a
ambil nilai b
jumlahkan
kembalikan 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.0
ldarg.1
add
ret

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

ldarg
ldloc
stloc
ldstr
call
callvirt
newobj
add
sub
br
brtrue
brfalse
ret

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 pertama
ambil argument kedua
jumlahkan
kembalikan hasil

Jika IL dari method tersebut dilihat menggunakan tool seperti ILSpy atau ildasm, bentuknya akan menyerupai:

Plain Text

ldarg.0
ldarg.1
add
ret

Tidak perlu menghafalkan instruksi tersebut.

Yang penting adalah memahami perjalanan abstraksinya:

Plain Text

C#:
return a + b;
IL:
ldarg.0
ldarg.1
add
ret
Native Machine Code:
instruksi yang sesuai
dengan 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

Type
Method
Field
Property
Parameter
Assembly 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 A
Bahasa B
└── Integer versi B
Bahasa 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
intSystem.Int32
longSystem.Int64
shortSystem.Int16
byteSystem.Byte
boolSystem.Boolean
charSystem.Char
floatSystem.Single
doubleSystem.Double
decimalSystem.Decimal
stringSystem.String
objectSystem.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

class
interface
delegate
array
string
object

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 type
Base type
Method
Property
Field
Interface
Accessibility
Parameter
Return 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 System
CLR
│ 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#

var
dynamic
nullable reference types
pattern 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

class
interface
struct
enum
delegate
array
object
string

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 / Executable
Component Application

Keduanya dapat memiliki:

Plain Text

IL
Metadata
Manifest
Type Information
References
Resources

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 Name
Version
Culture
Public Key / Strong Name Information

Informasi tersebut digunakan .NET untuk membedakan dan mengidentifikasi assembly.

Misalnya terdapat:

Plain Text

MyLibrary
Version 1.0.0.0

dan:

Plain Text

MyLibrary
Version 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

Images
Strings
Localization Resources
Embedded 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.dll
MyLibrary.dll
Logging.dll
Database.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
KepanjanganDynamic Link LibraryExecutable
Peran umumLibrary / dependencyExecutable application
Dapat berisi ILYaYa
Dapat berisi metadataYaYa
Dapat menjadi .NET assemblyYaYa
Umumnya memiliki entry point aplikasiTidakYa
Umumnya digunakan olehAplikasi atau library lainUser / 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 struktur
program 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 dibaca
saat 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.NET
F#

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 catch yang 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

Thread
Task
ThreadPool
Synchronization primitives
async / 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 + x64
Linux + x64
Linux + ARM64
macOS + 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

Process
Virtual Memory
File System
Network
Device
CPU Scheduling
Security

.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

Collections
File I/O
Networking
JSON
LINQ
HTTP
Reflection
Threading
Database access
dan 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.NET
F#

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#

if
for
class
interface
async
await

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

Compiler
CLR
JIT

Ketiganya memang berhubungan dengan execution program .NET, tetapi memiliki tanggung jawab yang berbeda.

Secara sederhana:

KomponenTanggung jawab utama
CompilerMengubah source code menjadi IL dan metadata
CLRMenyediakan environment untuk menjalankan managed code
JITMengubah 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 Loading
Type System
JIT Compilation
Garbage Collection
Exception Handling
Threading
Interop
Runtime 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 portable
JIT
Menghasilkan native code sesuai environment
CPU
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 Management
Garbage Collection
Type Safety
Exception Handling
Threading

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

CLR
CoreCLR
.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 Time
C# ─────────────────────→ 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#

if
for
class
interface
async
await

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

x64
ARM64

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
Execute
Subsequent 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

Inlining
Constant propagation
Dead code elimination
Devirtualization
Bounds-check optimizations
Register allocation
dan 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 A
Method B
Method 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 architecture
Runtime environment
Method usage
Type information
Execution 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:

JITAOT
Compilation dilakukan saat runtimeCompilation dilakukan sebelum runtime
Membutuhkan compilation saat aplikasi berjalanSebagian besar compilation sudah dilakukan sebelumnya
Dapat menggunakan informasi actual runtimeLebih banyak keputusan dibuat sebelum aplikasi berjalan
Dapat menimbulkan biaya JIT saat executionDapat mengurangi biaya compilation saat startup
Runtime melakukan lebih banyak pekerjaanSebagian pekerjaan dipindahkan ke build/publish

Secara sederhana:

Plain Text

JIT
Build Time Runtime
│ │
│ ↓
│ JIT
│ ↓
│ Native Code
│ ↓
└────────────→ CPU

Sedangkan:

Plain Text

AOT
Build 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 Tool
Serverless Function
Cloud Service
Mobile Application
Container
Small 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
Application
Application Start
Method dipanggil
JIT
Native Code
Execute

Dengan AOT:

Plain Text

Build
IL
AOT
Native Code
Application
Application 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 Management
Garbage Collection
Exception Handling
Threading
Type System
Interop
Runtime 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 Executable
Native Libraries
Runtime Components
Configuration
Resources
Dependencies

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 Work
AOT:
Build CPU
└── Compilation Work
Runtime 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

CPU
Execution Pattern
Method Usage
Type Information
Runtime 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 yang
diperlukan

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

Reflection
Dynamic Code
Runtime Type Discovery
Serialization
Dependency 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 → CPU
AOT:
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 runtime
AOT = semua compile time

.NET memiliki berbagai strategi compilation.

Terdapat:

Plain Text

JIT
Tiered Compilation
ReadyToRun
Native 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

class
method
string
object
Console

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:
Program
Method:
Main
Return Type:
void
Parameters:
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 Project
Build
Compile
Test
Publish
Run
Package
Manage Dependencies

Salah satu bagian yang paling sering digunakan adalah command-line interface:

Bash

dotnet

Contohnya:

Bash

dotnet new
dotnet restore
dotnet build
dotnet test
dotnet run
dotnet 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 Application
Runtime
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 new
dotnet restore
dotnet build
dotnet run
dotnet test
dotnet 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 Framework
Output Type
Dependencies
Build Configuration
Package References
Compiler 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

Console
Class Library
Web API
MVC
Razor
Worker
Test Project

dan berbagai template lainnya.

SDK dan Runtime dalam Development Machine

Sebuah mesin development biasanya membutuhkan SDK.

Misalnya ketika melakukan:

Bash

dotnet new
dotnet build
dotnet test
dotnet 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.x
Microsoft.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.100
10.0.200
10.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# Compiler
MSBuild
Project Templates
NuGet 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 build
dotnet run
dotnet test
dotnet 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

Development
dotnet build
dotnet test
dotnet publish
Execution
dotnet 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 new
dotnet build
dotnet run
dotnet test
dotnet 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.json
MyApp.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/run
publish
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.cs
SomeService.cs
UserController.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:

CommandTujuan
dotnet buildMengompilasi project
dotnet runBuild jika diperlukan lalu menjalankan aplikasi
dotnet publishMenyiapkan aplikasi untuk deployment

Contoh alurnya:

Plain Text

Development
├── dotnet build
│ ↓
│ Memastikan kode dapat dibangun
├── dotnet run
│ ↓
│ Menjalankan aplikasi
└── dotnet publish
Menyiapkan aplikasi
untuk 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 build
dotnet run
dotnet 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.cs
MyApp.csproj
Controllers/
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 clean
dotnet 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.cs
UserController.cs
OrderService.cs
ProductRepository.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.cs
ProductController.cs
ProductService.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", ...)
↓ Compiler
IL:
instruksi intermediate
↓ disimpan dalam
MyApp.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 aplikasi
Request 2
Compile seluruh aplikasi lagi
Request 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 compilation
Assembly
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:

DevelopmentDeployment
.cs source codeCompiled assembly
.csprojApplication configuration
Source files.dll
Development dependenciesRuntime 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 cores
16 GB RAM
SSD

Sedangkan production server mungkin memiliki:

Plain Text

32 CPU cores
128 GB RAM
High-performance storage
High-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 CPU
Tambah RAM
Tambah 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 A
CPU A
Windows
.NET Runtime
MyApp.dll

dan:

Plain Text

Server B
CPU 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 Runtime
Code 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

CPU
Memory
Disk
Network 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 CPU
Virtual RAM
Virtual Disk
Virtual Network
Guest 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:

KonsepFungsi
Physical MachineMenyediakan hardware sebenarnya
HypervisorMembuat dan mengelola virtual machine
Virtual MachineMenyediakan komputer virtual
Guest OSOperating system yang berjalan di dalam VM
.NET Runtime / CLRMenyediakan execution environment untuk managed .NET code
JITMengubah IL menjadi native machine code
CPUMengeksekusi 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 Virtualization
Server 1 → Application A
Server 2 → Application B
Server 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 MB
Container 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 → CPU
F# ──────┘

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

Bagian 1: Komputer Tidak Memahami C#
Bagian 3: C# Bukan .NET

Baca juga

Artikel sebelumnya

Artikel lain

Dibuat pelan-pelan dengan rasa hangat dari Kuningan.

© 2026 Rifky Haekal Al-Fadillah