Lewati ke konten utama

"Hello World!" dengan Zig

·1053 kata·5 menit
programming zig
deadManAlive
Penulis
deadManAlive
The dead man that does tell tales
Daftar isi

Zig merupakan bahasa pemrograman modern yang ditujukan sebagai alternatif dari C yang saat ini telah berusia 52 tahun dan segenap masalah-masalahnya seperti undefined behaviour, memory management yang penuh jebakan, macro yang hanya bekerja sebagai pemrosesan teks, dan sebagainya.

Manajemen memori eksplisit, type system yang lebih matang, dan eksekusi compile time merupakan beberapa cara Zig mengatasi permasalahan yang dihadapi oleh seorang pemrogram C. Selain itu, adanya interoperabilitas dengan kode C dan build system terpadu membuat bahasa ini menarik untuk pengembangan software modern berperforma tinggi.

Zig bukan merupakan bahasa pemrograman yang tepat untuk pemula. Setidaknya pemahaman dan pengalaman menggunakan C sangat disarankan sebelum mendalami Zig.

Persiapan
#

zig sebagai build system dari Zig dapat diunduh dari laman resmi Zig. Seperti compiler C, sebuah kode Zig berakhikran dengan .zig dan dapat dikompilasi dengan beberapa cara, seperti:

zig build-exe main.zig

atau langsung dieksekusi dengan:

zig run main.zig

“Hello, World!”
#

Untuk dapat menampilkan karakter pada shell menggunakan Zig, maka hal pertama yang harus dilakukan adalah:

const std = @import("std");

Perintah ini serupa dengan direktif #include pada C/C++. Di sini, fungsi built-in @import akan memuat file library standar (std) dan menyimpannya ke dalam variabel namespace std.

Untuk dapat mengeksekusi program, maka dibutuhkan fungsi main yang didefinisikan sebagai:

1
2
3
pub fn main() void {
    // ...
}

Fungsi main pada Zig dapat mengambalikan void (fn main() void {}) atau pun !void seperti pada contoh yang merupakan error union type, yang berarti fungsi ini dapat mengembalikan void atau pun suatu nilai error.

printf?
#

Tidak seperti pandangan awam yang melihatnya sebagai proses yang basic dan sederhana, proses printing karakter ke shell sebenarnya merupakan proses yang cukup rumit dan penuh pertimbangan seperti:

  1. Apakah menggunakan buffer? Tanpa buffer membuat syscall (pemanggilan fungsi tingkat sistem operasi) akan dilakukan pada setiap karakter sehingga mengurangi performa. Dengan buffer karakter-karakter dapat ditampung terlebih dahulu untuk kemudian dilakukan flusing sekaligus dengan memanggil satu syscall, meskipun karakter tidak tampil secara real-time.
  2. Bagaimana menangani error? Program dapat saja gagal untuk mengakses atau menulis ke stdout/stdin.
  3. Bagaimana mekanisme locking yang dibutuhkan jika printing dilakukan pada kondisi multi-threaded?
  4. Dan sebagainya.

Fungsi-fungsi seperti printf pada C, print pada Python, atau pun object std::cout/std::endl pada C++ mengabstraksikan hal-hal tersebut dari pemrogram dan menyerahkannya kepada kode-kode tersembunyi di standard library, hal ini juga yang membuat Rust menggunakan macro untuk fungsi print dan println.

Zig, dengan filosofi eksplisitnya, menyerahkan hal itu semua ke pemrogram. Oleh karena itu, terdapat beberapa cara untuk melakukan printing karakter ke shell.

Debug printing
#

std.debug.print merupakan fungsi yang menyerupai printf pada C. Sesuai dengan lokasinya pada namespace debug, fungsi ini sebaiknya hanya digukan pada saat pengembangan, bukan pada kode produksi. Hal ini disebabkan karena fungsi ini menulis ke stderr (alih-alih stdout), melakukan locking (yang tidak dibutuhkan pada kondisi single-threaded), tidak menggunakan buffer (sehingga mempengaruhi performa jika dilakukan setiap saat), dan juga mengabaikan error.

Kode jika menggunakan fungsi ini:

const std = @import("std");

pub fn main() void {
    std.debug.print("Hello, World!\n", .{});
}
Hanya gunakan fungsi ini untuk melakukan debugging.

Printing manual ke stdout
#

Zig tidak menyediakan fungsi khusus untuk melakukan printing karakter ke stdout, sehingga pemrogram harus mengirim data karakter secara manual ke file descriptor tersebut. Hal ini menggunakan fungsi std.io.getStdOut()yang mengembalikan objek stdout, yang memiliki tipe std.fs.File. Tipe tersebut memiliki method writer() yang mengembalikan objek Writer yang dapat digunakan mengirim data karakter ke objek stdout tersebut.

Untuk itu, langkah pertama yang dilakukan adalah menginisiasi objek Writer dari stdout ke dalam variabel stdout_writer:

const std_out = std.io.getStdOut().writer();

Selanjutnya, printing dapat dilakukan dengan memanggil method print() dari objek Writer tersebut:

try stdout_writer.print("Hello, World!\n", .{});

Digunakan keyword try karena method print() mengembalikan tipe error union, dalam hal ini Error!void. Keyword try dapat digunakan sebagai salah satu cara menghandle tipe error union ini dengan cara meneruskannya ke fungsi yang memanggil method print() ini, yaitu fungsi main(). Hal tersebut membuat fungsi main() harus memiliki tipe kembalian !void.

Argument kedua (.{}) pada fungsi-fungsi .print di atas merupakan cara zig melakukan varidic argument seperti pada C.

Kode program akan terlihat seperti di bawah:

const std = @import("std");

pub fn main() !void {
    const stdout_writer = std.io.getStdOut().writer();
    try stdout_writer.print("Hello, World!\n", .{});
}

Menggunakan buffer
#

Meskipun cara di atas berhasil membuat program yang menampilkan karakter ke stdout, program tersebut memiliki kekurangan dimana setiap printing karakter akan memicu syscall (system call) berupa pemanggilan fungsi sistem yang bertanggung jawab untuk menampilkan karakte-karakter tersebut ke layar. Syscall setiap saat ini dapat memberikan dampak ke performa program, terutama apabila printing dilakukan setiap saat dan berulang.

Salah satu cara untuk menangani hal ini adalah menggunakan buffer, yang menyimpan semua karakter yang akan diprint dan kemudian melakukan syscall sekali saja ketika semua karakter pada buffer akan di print.

std.io.bufferedWriter() merupakan fungsi yang dapat digunakan untuk menginisiasi sebuah BufferedWriter, yaitu sebuah objek yang dapat mewrap sebuah Writer dengan buffer. Dengan menggunakan kembali stdout_writer pada contoh di atas, maka sebuah BufferedWriter dapat diinisiasi dengan:

var buff_writer = std.io.bufferedWriter(stdout_writer);

Digunakan var untuk menginisiasi objek ini karena buffer dari objek akan diisi selama runtime.

Selajutnya, dari objek buff_writer diperoleh objek Writer pula untuk mengirim data dari sisi user:

const zigout = buff_writer.writer();

Yang kemudian dapat digunakan untuk menampilkan data ke shell:

zigout.print("Hello, World!\n", .{});

Tetapi, teks tersebut tidak akan dikirim ke stdout sebelum buffer diflush secara manual untuk mengisi stdout, dengan cara:

buff_writer.flush();

Karena kedua fungsi terakhir mengambalikan error union, maka error handling dapat dialihkan ke fungsi main dengan cara:

const std = @import("std");

pub fn main() !void {
    const stdout_writer = std.io.getStdOut().writer();
    var buff_writer = std.io.bufferedWriter(stdout_writer);
    const zigout = buff_writer.writer();

    try zigout.print("Hello, World!\n", .{});

    try buff_writer.flush();
}

Namun, efek dari penggunaan buffer akan lebih terasa ketika melakukan printing dengan jumlah besar. Bandingkan waktu eksekusi kedua program berikut yang melakukan printing terhadap Hello, World! (\(i\)) sebanyak 10000 kali:

//! naive.zig

const std = @import("std");

pub fn main() !void {
    const stdout_writer = std.io.getStdOut().writer();

    for (0..10000) |i| {
        try stdout_writer.print("Hello, World! ({})\n", .{i});
    }
}
//! buffered.zig

const std = @import("std");

pub fn main() !void {
    const stdout_writer = std.io.getStdOut().writer();
    var buff_writer = std.io.bufferedWriter(stdout_writer);
    const zigout = buff_writer.writer();

    for (0..10000) |i| {
        try zigout.print("Hello, World! ({})\n", .{i});
    }

    try buff_writer.flush();
}

Kedua code kemudian dicompile dengan zig build-exe FILE.zig dan waktu eksekusi dihitung dengan command Unix time. Penulis mendapatkan angka seperti berikut:

KodeWaktu (detik)
naive.zig\(1.3543952\)
buffered.zig\(0.0860223\)

Dapat dilihat terdapat pengurangan waktu eksekusi hingga 16 kali dengan menggunakan dibanding dengan tanpa buffer.

Hello, World!