Menyusun operasi stream

Menyusun wrapper generik atas sumber dan tujuan apa pun.

Daftar isi

Operasi stream adalah pembacaan atau penulisan data secara berurutan tanpa ukuran akhir yang diketahui di muka, dan std.Io menyediakan primitifnya dalam bentuk Reader dan Writer. Topik ini membahas dua konstruktor untuk sumber dan tujuan di memori, penyusunan fungsi salin generik dengan streamRemaining, serta pengambilan seluruh sisa data dengan allocRemaining. Setelah membacanya, satu fungsi salin yang sama dapat dipakai untuk pasangan sumber dan tujuan apa pun, dari memori hingga berkas.

Konsep#

Memori sebagai sumber dan tujuan#

Tidak semua data berasal dari berkas atau jaringan. Data uji, teks konstanta, dan hasil konversi berada langsung di memori, sehingga std.Io menyediakan dua konstruktor khusus untuknya. std.Io.Reader.fixed menghasilkan Reader atas sebuah slice yang tidak berubah, sedangkan std.Io.Writer.fixed menghasilkan Writer yang menulis ke buffer milik pemanggil. Keduanya tidak memerlukan instance io karena tidak ada sumber luar yang dijangkau, dan keduanya tidak mengalokasikan memori baru.

zig Bentuk umum Reader.fixed dan Writer.fixed
// tidak-dijalankan
var r = std.Io.Reader.fixed("data konstan");
var w = std.Io.Writer.fixed(&tujuan);

Karena tujuan Writer.fixed adalah memori, tulisannya dapat diperiksa kapan saja melalui slice buffer itu sendiri. Medan end pada antarmuka menyimpan jumlah byte yang sudah tertulis, sehingga tujuan[0..w.end] adalah seluruh isi yang sah. Pemanggilan flush tetap diperbolehkan, tetapi tidak mengirim apa pun ke mana pun karena datanya memang sudah berada di memori. Sifat ini menjadikan pasangan fixed sebagai sarana pengujian logika I/O tanpa menyentuh sistem berkas sama sekali.

Kedua konstruktor menuntut data yang hidup lebih lama daripada antarmukanya. Slice yang diberikan ke Reader.fixed tetap dirujuk selama Reader dipakai, dan buffer yang diberikan ke Writer.fixed berlaku sama, sehingga keduanya sebaiknya dideklarasikan pada fungsi yang memakai antarmukanya sampai selesai. Batasan ini sejalan dengan buffer pada File.Writer yang dibangun atas berkas, dan merupakan konsekuensi desain yang tidak mengalokasikan memori baru. Pola pasangan fixed dipakai untuk menyiapkan keluaran kecil sebelum dikirim, memecah teks konstanta, dan menguji fungsi generik tanpa menyiapkan berkas.

Contoh Lengkap#

Fungsi salin generik dengan streamRemaining#

Fungsi streamRemaining menerima *std.Io.Reader dan *std.Io.Writer, lalu menyalin data dari sumber ke tujuan sampai sumbernya habis, dan mengembalikan jumlah byte yang tersalin. Perulangan internalnya menangani error.EndOfStream sebagai tanda berhenti, bukan sebagai kegagalan. Karena parameternya berupa antarmuka, fungsinya tidak mengetahui asal data maupun jenis tujuannya, dan perulangan penyalinan tidak perlu ditulis ulang di setiap program.

Sifat itu memungkinkan wrapper, yaitu fungsi pembungkus yang menerima antarmuka lalu menyusun operasi di atasnya. Fungsi salin pada program berikut hanyalah penerusan panggilan streamRemaining, tetapi tanda tangannya sudah mencakup semua pasangan sumber dan tujuan yang ada maupun yang akan datang. Dua pemanggilan pada main membuktikan hal itu dengan sumber yang berbeda dan penampung yang sama.

zig salin-catatan.zig
const std = @import("std");

fn salin(r: *std.Io.Reader, w: *std.Io.Writer) !usize {
    return std.Io.Reader.streamRemaining(r, w);
}

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const arena = init.arena.allocator();

    var out_buf: [512]u8 = undefined;
    var stdout_writer = std.Io.File.stdout().writer(io, &out_buf);
    const out = &stdout_writer.interface;

    const catatan = "suhu=27.5\nlembap=61%\ntekanan=1009 hPa\n";

    var tujuan: [64]u8 = undefined;
    var penampung = std.Io.Writer.fixed(&tujuan);
    var sumber = std.Io.Reader.fixed(catatan);
    const jumlah = try salin(&sumber, &penampung);
    try out.print("tersalin {d} byte\n", .{jumlah});
    try out.writeAll(tujuan[0..penampung.end]);

    var sumber_kedua = std.Io.Reader.fixed("laporan harian siang\n");
    var penampung_kedua = std.Io.Writer.fixed(&tujuan);
    const jumlah_kedua = try salin(&sumber_kedua, &penampung_kedua);
    try out.print("tersalin {d} byte\n", .{jumlah_kedua});
    try out.writeAll(tujuan[0..penampung_kedua.end]);

    var sumber_ketiga = std.Io.Reader.fixed(catatan);
    const salinan = try sumber_ketiga.allocRemaining(arena, .unlimited);
    try out.print("dialokasikan {d} byte\n", .{salinan.len});

    try out.flush();
}
text Keluaran
tersalin 38 byte
suhu=27.5
lembap=61%
tekanan=1009 hPa
tersalin 21 byte
laporan harian siang
dialokasikan 38 byte

Panggilan pertama menyalin 38 byte dari teks catatan ke buffer tujuan, dan isinya dicetak melalui tujuan[0..penampung.end]. Panggilan kedua memakai fungsi salin yang sama atas sumber lain, kemudian menimpa buffer yang sama dengan 21 byte hasil baru. Bagian akhir program memanggil allocRemaining, yang dibahas pada bagian berikutnya.

streamRemaining bekerja dalam potongan, sehingga sumber tidak perlu utuh di memori. Data dipindahkan dari buffer Reader ke buffer Writer sepanjang perulangannya, dan jumlah byte yang dikembalikan dapat dipakai untuk pelaporan progres maupun pencocokan ukuran. Apabila tujuan kehabisan ruang, error dari Writer naik ke pemanggil, sehingga penampung berukuran tetap ditentukan dengan ukuran terbesar yang diharapkan.

Jalankan program dari direktori tempat salin-catatan.zig berada.

bash
# jalankan program dari direktori topik
zig run salin-catatan.zig
text Keluaran
tersalin 38 byte
suhu=27.5
lembap=61%
tekanan=1009 hPa
tersalin 21 byte
laporan harian siang
dialokasikan 38 byte

Membaca seluruh sisa dengan allocRemaining#

Metode allocRemaining mengambil seluruh sisa data dari sebuah Reader dan mengembalikan slice []u8 milik pemanggil yang dialokasikan melalui allocator yang diberikan. Dengan arena allocator dari init.arena, pembebasannya otomatis pada akhir proses, sehingga tidak ada pemanggilan pembersihan yang diperlukan. Hasilnya berupa memori kontigu, sehingga siap diolah sebagai teks maupun data biner tanpa penyalinan tambahan.

Parameter kedua menetapkan batas pembacaan berupa std.Io.Limit. Nilai .unlimited membebaskan pembacaan tanpa batas, sedangkan std.Io.Limit.limited(n) menghentikan pembacaan dengan error.StreamTooLong ketika data mencapai atau melampaui batas tersebut. Batas semacam ini penting ketika sumber data berada di luar kendali program, misalnya masukan pengguna atau berkas yang ukurannya tidak diketahui, karena program menolak memakan memori tanpa henti.

zig Contoh: allocRemaining dengan batas
// tidak-dijalankan
const salinan = r.allocRemaining(arena, std.Io.Limit.limited(1024)) catch |err| switch (err) {
    error.StreamTooLong => return error.CatatanTerlaluPanjang,
    else => return err,
};

Pemilihan metode pengambilan data mengikuti bentuk masalahnya. Apabila ukuran data sudah diketahui, buffer tetap berukuran tetap dengan readSliceShort lebih hemat daripada alokasi dinamis. Metode allocRemaining unggul ketika ukuran akhir baru diketahui setelah seluruh data terbaca, misalnya pada berkas catatan yang terus bertambah, dan keduanya bekerja atas Reader yang sama tanpa mengubah kode di sekitarnya.

Slice yang dikembalikan dimiliki pemanggil sepenuhnya, sehingga pihak yang memanggil menanggung pembersihannya. Dengan arena, tanggung jawab itu berlaku sekali di akhir proses, sedangkan dengan allocator lain seperti allocator pengujian, pembebasan manual wajib dilakukan agar pemeriksa kebocoran tidak melaporkan sisa memori. Aturan kepemilikan ini sama dengan aturan umum alokasi Zig, karena allocRemaining hanyalah alokasi biasa yang isinya diisi dari stream.

Kesalahan umum#

Kesalahan pertama berupa mengira buffer Reader otomatis menampung seluruh sumber. Buffer berukuran tetap hanya menyimpan potongan, dan sisa datanya diambil pada panggilan berikutnya. Program yang membaca berkas besar dengan buffer 256 byte tetap benar selama perulangannya menghormati nilai kembalian setiap pembacaan.

Kesalahan kedua berupa lupa membebaskan hasil allocRemaining. Metode ini mengalokasikan memori sesuai sisa data, dan slice hasilnya wajib dibebaskan pada allocator yang sama setelah dipakai. Pemeriksaan kebocoran pada modul Memori dan allocator menangkap kelupaan ini saat zig test dijalankan.

Ringkasan#

  • Reader.fixed dan Writer.fixed menyediakan Reader dan Writer atas memori tanpa instance io dan tanpa alokasi baru.
  • streamRemaining menyalin seluruh data antar antarmuka dan mengembalikan jumlah byte, sehingga satu fungsi generik melayani semua pasangan sumber dan tujuan.
  • allocRemaining mengambil seluruh sisa data ke slice milik pemanggil, dengan std.Io.Limit sebagai batas dan error.StreamTooLong ketika batas tercapai.
  • Medan end pada Writer menyimpan jumlah byte tertulis untuk memeriksa hasil penulisan ke buffer.

Referensi#