Callback

Proc Crystal sebagai function pointer yang dipanggil balik oleh kode C.

Daftar isi

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.

crystal urut.cr
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(", ")
bash
# qsort berasal dari libc sehingga tidak perlu kompilasi tambahan
crystal run urut.cr
text Keluaran
3, 7, 19, 25, 42

Parameter 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.

c panggil.c
// 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);
    }
}
crystal callback.cr
@[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)
bash
# kompilasi pustaka C menjadi berkas objek
gcc -c -o panggil.o panggil.c
# jalankan program Crystal yang meneruskan proc ke C
crystal run callback.cr
text Keluaran
25
256

Parameter 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.

crystal kotak.cr
@[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
bash
# pastikan panggil.o tersedia, lalu jalankan programnya
gcc -c -o panggil.o panggil.c
crystal run kotak.cr
text Keluaran
28

Callback 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.box mengubah objek menjadi Void* untuk parameter data pengguna, dan Box(T).unbox mengembalikannya bertipe T.
  • Proc callback harus tetap hidup selama sisi C masih menyimpan dan memanggilnya.

Referensi#