Banyak pustaka C menerima sebuah fungsi sebagai argumen, misalnya untuk membandingkan elemen saat mengurutkan atau untuk memberi tahu pemanggil setiap kali sebuah peristiwa terjadi. Fungsi semacam itu disebut callback, yaitu kode yang diteruskan ke pustaka agar dapat dipanggil balik oleh pustaka tersebut. Di sisi Crystal, peran callback diisi oleh Proc. Topik ini membahas deklarasi parameter callback pada fun, penerusan Proc ke pustaka C buatan sendiri, dan penyampaian data tambahan ke callback melalui Box.
Komparator untuk qsort#
Fungsi qsort dari libc mengurutkan array dengan membandingkan dua elemen melalui fungsi yang diberikan pemanggil. Parameter pembandingnya dideklarasikan sebagai tipe proc, sehingga compiler Crystal dapat memeriksa kesesuaian proc yang diteruskan. Program berikut mendeklarasikan ulang qsort dalam blok lib milik program karena simbol tersebut belum tersedia sebagai LibC.qsort, lalu mengurutkan array Int32 dengan komparator buatannya sendiri.
lib Urut
fun qsort(base : Void*, nmemb : LibC::SizeT, size : LibC::SizeT, compar : (Void*, Void*) -> Int32) : Void
end
komparator = ->(a : Void*, b : Void*) : Int32 {
Pointer(Int32).cast(a).value <=> Pointer(Int32).cast(b).value
}
data = [42, 7, 19, 3, 25] of Int32
Urut.qsort(data.to_unsafe, data.size.to_u64, sizeof(Int32).to_u64, komparator)
puts data.join(", ")# qsort berasal dari libc sehingga tidak perlu kompilasi tambahan
crystal run urut.cr3, 7, 19, 25, 42Parameter compar bertipe (Void*, Void*) -> Int32, yaitu proc yang menerima dua alamat memori dan mengembalikan bilangan bertanda. Saat mengurutkan, qsort menyerahkan alamat dua elemen kepada komparator, sehingga Pointer(Int32).cast(a).value membaca nilai Int32 pada alamat tersebut sebelum keduanya dibandingkan dengan operator <=>. Nilai balik komparator mengikuti kontrak <=>, yaitu negatif apabila elemen pertama lebih kecil, nol apabila sama, dan positif apabila lebih besar. Argumen nmemb dan size bertipe LibC::SizeT, sehingga jumlah elemen dan ukuran elemennya dikonversi dengan to_u64.
Metode data.to_unsafe memberikan Pointer(Int32) yang menunjuk ke penampung internal array, sehingga qsort bekerja langsung pada array Crystal tanpa menyalin isinya. Pengurutan dilakukan di tempat, dan urutan cetakannya berubah tepat setelah pemanggilan. Array data tetap hidup selama pemanggilan berlangsung karena variabelnya masih dipegang ruang lingkup yang sama, sehingga alamat yang diteruskan tidak pernah menggantung.
Pustaka C yang memanggil balik#
Contoh berikut membalik arahnya, yaitu pustaka C buatan sendiri memegang Proc Crystal dan memanggilnya berulang kali. Berkas C berikut memuat dua fungsi. Fungsi terapkan_tiga_kali menerima pointer fungsi dan memanggilnya tiga kali berturut-turut atas nilai awalnya, sedangkan fungsi jumlahkan digunakan pada bagian akhir topik ini. Pembacaan int (*f)(int) adalah cara C menuliskan parameter berupa pointer ke fungsi yang menerima int dan mengembalikan int, dan bentuk inilah yang dijumpai pada banyak pustaka C nyata, misalnya untuk fungsi pembanding atau penangan peristiwa.
// tidak-dijalankan
int terapkan_tiga_kali(int (*f)(int), int awal) {
int hasil = awal;
for (int i = 0; i < 3; i++) {
hasil = f(hasil);
}
return hasil;
}
void jumlahkan(const int *data, int n, void (*kunjungi)(int, void *), void *konteks) {
for (int i = 0; i < n; i++) {
kunjungi(data[i], konteks);
}
}@[Link(ldflags: "#{__DIR__}/panggil.o")]
lib Panggil
fun terapkan_tiga_kali(f : (Int32 -> Int32), awal : Int32) : Int32
end
puts Panggil.terapkan_tiga_kali(->(x : Int32) { x + 5 }, 10)
puts Panggil.terapkan_tiga_kali(->(x : Int32) { x * x }, 2)# kompilasi pustaka C menjadi berkas objek
gcc -c -o panggil.o panggil.c
# jalankan program Crystal yang meneruskan proc ke C
crystal run callback.cr25
256Parameter f bertipe Int32 -> Int32, yaitu proc yang menerima Int32 dan mengembalikan Int32, padanan int (*f)(int) pada deklarasi C. Pemanggilan pertama meneruskan proc penambah lima, sehingga 10 menjadi 15, lalu 20, lalu 25. Pemanggilan kedua meneruskan proc pengkuadrat, sehingga 2 menjadi 4, 16, dan 256. Keputusan berapa kali fungsi dipanggil berada di sisi C, sedangkan isi perhitungannya berada di sisi Crystal.
Data pengguna dengan Box#
Banyak API C menyediakan parameter void* untuk membawa data pengguna (user data) menuju callback. Pihak C tidak mengetahui isinya, sehingga objek Crystal yang diteruskan melalui parameter itu harus dibungkus terlebih dahulu. Box.box(objek) mengubah objek menjadi Void*, dan Box(T).unbox di dalam callback mengembalikannya bertipe T. Program berikut menjumlahkan array melalui fungsi jumlahkan dari panggil.c, dengan objek penampung total yang dibungkus Box sebelum diteruskan.
@[Link(ldflags: "#{__DIR__}/panggil.o")]
lib Panggil
fun jumlahkan(data : Int32*, n : Int32, kunjungi : (Int32, Void*) -> Void, konteks : Void*) : Void
end
class Total
property nilai = 0
end
data = [3, 8, 5, 12] of Int32
total = Total.new
callback = ->(nilai : Int32, konteks : Void*) do
kotak = Box(Total).unbox(konteks)
kotak.nilai += nilai
end
Panggil.jumlahkan(data.to_unsafe, data.size, callback, Box.box(total))
puts total.nilai# pastikan panggil.o tersedia, lalu jalankan programnya
gcc -c -o panggil.o panggil.c
crystal run kotak.cr28Callback menerima setiap elemen beserta pointer konteksnya, membuka kotaknya dengan Box(Total).unbox, lalu menambahkan nilai elemen ke objek Total yang sama. Karena Total adalah class, yang dibungkus Box adalah referensinya, sehingga penambahan dari dalam callback terbaca oleh variabel total di luar. Setelah perulangan di sisi C selesai, jumlah 3, 8, 5, dan 12 tercetak sebagai 28.
Kehati-hatian muncul apabila nilai yang dibungkus bukan tipe referensi. Box.box atas nilai seperti Int32 menyalin nilainya ke memori tersendiri, sehingga perubahan dari dalam callback tidak menjangkau variabel aslinya. Karena itu pola yang lazim adalah membungkus objek berkeadaan seperti Total, lalu mengubah keadaan objeknya dari dalam callback.
Kesalahan umum#
Kesalahan pertama adalah meneruskan Proc yang tipenya tidak cocok dengan deklarasi fun, misalnya komparator yang menerima Int32 langsung padahal qsort memberikan Void*. Pemanggilan seperti ini ditolak compiler karena parameternya bertipe (Void*, Void*) -> Int32. Perbaikannya adalah mengikuti tipe yang dideklarasikan dan membaca nilai elemen melalui Pointer(Int32).cast seperti pada urut.cr.
Kesalahan kedua adalah memanggil Box.unbox tanpa menyebut tipenya. Pemanggilan Box.unbox(konteks) ditolak compiler dengan pesan can't infer the type parameter T for the generic class Box(T), karena tipe yang dibuka memang tidak dapat ditebak dari pointer Void*. Perbaikannya adalah menyebut tipenya secara eksplisit menjadi Box(Total).unbox(konteks).
Kesalahan ketiga adalah menjalankan program Crystal tanpa mengompilasi pustaka C-nya terlebih dahulu. Error baru muncul pada tahap link dengan pesan undefined reference yang menyebut nama fungsi pustaka. Perbaikannya adalah menjalankan gcc -c -o panggil.o panggil.c sebelum crystal run, sebagaimana urutan perintah pada blok bash di atas.
Ringkasan#
- Parameter callback dideklarasikan sebagai tipe proc pada
fun, dan compiler memeriksa kesesuaian Proc yang diteruskan. - Fungsi C dapat memanggil Proc Crystal berulang kali, sehingga kendali perulangan berada di sisi C sedangkan perhitungannya di sisi Crystal.
Box.boxmengubah objek menjadiVoid*untuk parameter data pengguna, danBox(T).unboxmengembalikannya bertipe T.- Proc callback harus tetap hidup selama sisi C masih menyimpan dan memanggilnya.
Referensi#
- The Crystal Programming Language Reference — crystal-lang.org — Dokumentasi resmi bahasa dengan bagian C bindings yang membahas callback dan pola Box.
- Dokumentasi API Crystal 1.22.1 — crystal-lang.org — Rujukan pustaka standar untuk Proc dan Box.