Pengenalan Source Generator

Peta generasi kode pada waktu kompilasi.

Daftar isi

Topik ini memetakan cara kerja source generator (generator kode sumber), yaitu program yang menuliskan kode C# lain pada compile time (waktu kompilasi). Pembahasan mencakup atribut penanda, kerangka kode generator, pendaftaran generator pada proyek, serta hasil akhirnya yang berupa berkas kode biasa. Setelah topik ini diselesaikan, kode yang dihasilkan generator dapat dikenali dari bentuknya dan alur pembuatannya dapat diikuti dari ujung ke ujung, tanpa harus menulis generator sendiri.

Apa yang dilakukan source generator#

Source generator adalah komponen yang berjalan bersama kompilator C# saat proyek dibangun, membaca kode aplikasi, lalu menambahkan berkas kode sumber baru ke dalam kompilasi yang sama. Pustakanya dibangun di atas Roslyn, yaitu pustaka kompilator C# milik .NET yang juga dipakai editor untuk analisis kode. Berkas yang ditulis generator umumnya diberi akhiran .g.cs dan diompilasi bersama kode aplikasi, sehingga kodenya memperoleh pemeriksaan tipe, dukungan debug, dan dukungan editor yang sama seperti kode yang ditulis manusia.

Cara kerjanya berbeda dari refleksi pada runtime (waktu program berjalan). Refleksi membaca bentuk tipe saat program sudah berjalan, sedangkan generator menyiapkan kodenya sebelum program berjalan, sehingga tidak ada biaya pemeriksaan tipe saat eksekusi dan hasilnya dapat dipakai pada skenario ahead-of-time yang membatasi refleksi. Fitur bawaan .NET memakai teknik ini pada generator JSON System.Text.Json dan generator pola teks System.Text.RegularExpressions. Alur satu generator dapat diringkas dalam empat langkah berikut.

  1. Kode aplikasi diberi atribut penanda pada enum, kelas, atau tipe lain yang ingin diproses.
  2. Generator yang terdaftar pada proyek membaca pohon sintaks kompilasi dan mencari tipe yang bertanda tersebut.
  3. Generator menyusun teks kode baru dan menambahkannya sebagai berkas .g.cs ke dalam kompilasi.
  4. Kompilator mengompilasi berkas hasil tersebut bersama kode aplikasi menjadi satu program.

Atribut penanda dan kerangka generator#

Atribut penanda adalah jangkar yang menghubungkan kode aplikasi dengan generator. Generator tidak memindai seluruh kode secara membabi buta, melainkan mencari tipe yang dipasangi atribut tertentu, sehingga pemakaian generatornya terbaca langsung dari kode aplikasi. Contoh berikut menetapkan atribut TampilkanAttribute untuk enum, lalu memasangnya pada enum Status.

csharp Contoh: atribut penanda pada enum
// tidak-dijalankan
// Atribut penanda yang dicari generator pada waktu kompilasi.
[AttributeUsage(AttributeTargets.Enum)]
sealed class TampilkanAttribute : Attribute
{
}

// Enum yang diberi atribut ini akan diproses generator.
[Tampilkan]
enum Status
{
    Aktif,
    Nonaktif,
    Tertunda
}

Kode generatornya sendiri adalah kelas biasa yang mengimplementasikan antarmuka IIncrementalGenerator dari Roslyn. Kelas tersebut mendaftarkan dua hal, yaitu penyaring sintaks yang memilih simpul bertanda atribut dan penulisan berkas keluaran untuk setiap simpul yang lolos. Kerangkanya terlihat seperti berikut, dengan isi metode Tampilkan ditulis oleh generator, bukan oleh manusia.

csharp Contoh: kerangka kode generator
// tidak-dijalankan
// Generator memerlukan proyek netstandard2.0 dengan paket Microsoft.CodeAnalysis.CSharp.
[Generator]
public sealed class GeneratorStatus : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var enumBertanda = context.SyntaxProvider.ForAttributeWithMetadataName(
            fullyQualifiedMetadataName: "TampilkanAttribute",
            predicate: static (simpul, _) => simpul is EnumDeclarationSyntax,
            transform: static (konteks, _) => (EnumDeclarationSyntax)konteks.TargetNode);

        context.RegisterSourceOutput(enumBertanda, static (konteks, enumDeklarasi) =>
        {
            // susun teks kode dari nama dan anggota enum, lalu tambahkan sebagai berkas:
            // konteks.AddSource("StatusText.g.cs", teksKode);
        });
    }
}

Metode ForAttributeWithMetadataName menyaring simpul sintaks yang dipasangi atribut TampilkanAttribute, sedangkan RegisterSourceOutput menuliskan hasilnya lewat AddSource. Teks yang disusun generator hanyalah string berisi kode C# biasa, sehingga isi berkas .g.cs dapat dibaca seperti berkas kode lain.

Mendaftarkan generator pada proyek#

Generator tidak bekerja dengan sendirinya. Proyek aplikasi harus merujuk proyek generator dengan konfigurasi khusus yang menyatakan bahwa rujukan itu adalah analyzer, yaitu komponen yang dipasang pada jalur kompilasi. Konfigurasinya ditulis pada berkas .csproj aplikasi.

xml Contoh: rujukan generator pada berkas .csproj aplikasi
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="../GeneratorStatus/GeneratorStatus.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
  </ItemGroup>

</Project>

Dua atribut pada ProjectReference itulah bagian pentingnya. Nilai OutputItemType="Analyzer" menyuruh sistem build memuat proyek generator sebagai komponen kompilasi, sedangkan ReferenceOutputAssembly="false" menyatakan bahwa kode generator tidak digabungkan ke aplikasi sehingga aplikasi tidak membawanya saat dipasang. Dengan pendaftaran itu, setiap kali aplikasi dibangun, generator ikut dijalankan dan berkas hasilnya langsung ikut dikompilasi tanpa langkah manual tambahan. Proyek generator sendiri menargetkan netstandard2.0 dan merujuk paket Microsoft.CodeAnalysis.CSharp yang menyediakan API Roslyn-nya.

Hasil yang dapat dieksekusi#

Generator memerlukan proyek Roslyn terpisah dan proses build yang lengkap, sehingga proses pembuatannya tidak dijalankan di sini. Yang dapat dieksekusi adalah hasilnya. Program berikut meniru proyek kecil yang telah dipasangi generator: enum Status diberi atribut penanda, sedangkan kelas StatusText adalah isi berkas StatusText.g.cs yang pada proyek nyata ditulis generator pada waktu kompilasi. Untuk keperluan demo, bagian hasil itu ditulis manual agar programnya dapat dijalankan utuh.

csharp Program.cs
// Program ini meniru hasil kerja source generator pada waktu kompilasi.
// Bagian berlabel "hasil generator" biasanya tidak ditulis manusia.

Status kini = Status.Aktif;
Status berikutnya = Status.Tertunda;

Console.WriteLine(StatusText.Tampilkan(kini));
Console.WriteLine(StatusText.Tampilkan(berikutnya));

// Kode yang ditulis manusia: enum yang diberi atribut penanda.
[Tampilkan]
enum Status
{
    Aktif,
    Nonaktif,
    Tertunda
}

// ---- hasil generator: berkas StatusText.g.cs pada proyek nyata ----
static class StatusText
{
    public static string Tampilkan(Status status) => status switch
    {
        Status.Aktif => "Aktif",
        Status.Nonaktif => "Nonaktif",
        Status.Tertunda => "Tertunda",
        _ => throw new ArgumentOutOfRangeException(nameof(status))
    };
}

// Atribut penanda: pada proyek nyata milik proyek generator.
[AttributeUsage(AttributeTargets.Enum)]
sealed class TampilkanAttribute : Attribute
{
}
text Keluaran
Aktif
Tertunda

Metode Tampilkan mengubah setiap angka enum menjadi teksnya tanpa refleksi, dan peta pemetaannya sudah jadi sebelum program berjalan. Manfaat pola ini terlihat dari dua sisi. Pada waktu kompilasi, penambahan anggota enum yang lupa dipetakan dapat dibuat menjadi error oleh generator, sedangkan lapisan refleksi yang setara hanya menemukan masalah itu saat program berjalan. Pada waktu eksekusi, tidak ada pemeriksaan tipe dinamis yang diulang setiap pemanggilan, sehingga kode hasilnya sama cepatnya dengan kode yang ditulis manual.

Kesalahan umum#

  • Menganggap generator mengubah berkas yang sudah ada. Generator hanya menambahkan berkas baru ke dalam kompilasi dan tidak menyunting kode yang ditulis manusia. Apabila hasil yang diinginkan berubah, yang disunting adalah teks yang disusun generator, bukan berkas aplikasinya.
  • Mencari hasil generator saat program berjalan. Proses generasi terjadi pada waktu kompilasi, sehingga kode hasilnya tidak ada pada runtime dan tidak muncul lewat refleksi. Apabila tipe hasilnya tidak ditemukan, periksa rujukan OutputItemType="Analyzer" pada .csproj dan atribut penandanya.
  • Menyunting berkas .g.cs secara manual. Berkas hasil generator ditimpa pada setiap build, sehingga suntingan langsung hilang tanpa jejak. Perbaikan dilakukan pada kode generatornya, lalu build dijalankan ulang.

Ringkasan#

  • Source generator menuliskan berkas kode sumber baru pada waktu kompilasi melalui API Roslyn, dan hasilnya diompilasi bersama kode aplikasi.
  • Atribut penanda menandai tipe yang diproses, sedangkan kelas generator menyaring sintaks dan menambahkan berkas .g.cs lewat AddSource.
  • Rujukan proyek dengan OutputItemType="Analyzer" dan ReferenceOutputAssembly="false" mendaftarkan generator pada aplikasi.
  • Kode hasil bebas refleksi runtime, dapat diperiksa kompilator, dan dapat dibuat error apabila ada anggota enum yang belum dipetakan.

Referensi#