Pustaka C yang sesungguhnya jarang berhenti pada fungsi dengan argumen angka. Data biasanya dikemas dalam struct, dan fungsi C menerimanya sebagai salinan atau sebagai pointer, yaitu nilai yang menyimpan alamat memori. Topik ini membahas cara mendeklarasikan struct C di sisi Crystal, meneruskannya ke C melalui kedua jalur tersebut, dan mengelola blok memori yang dialokasikan langsung dari Crystal. Hasil akhirnya adalah data yang dapat ditulis dan dibaca dari kedua bahasa.
Struct C di dalam lib#
Struct C dideklarasikan di dalam blok lib dengan kata kunci struct dan daftar field bertipe. Urutan dan tipe field harus sama dengan deklarasi C karena compiler menyusun keduanya mengikuti layout yang sama, yakni susunan byte di memori. Berkas C berikut memuat struct Titik dengan dua field double, satu fungsi yang menerimanya sebagai salinan, dan satu fungsi yang menerimanya sebagai pointer.
// tidak-dijalankan
#include <math.h>
struct Titik {
double x;
double y;
};
double jarak_nol(struct Titik t) {
return sqrt(t.x * t.x + t.y * t.y);
}
void geser(struct Titik *t, double dx, double dy) {
t->x += dx;
t->y += dy;
}Fungsi jarak_nol menerima struct Titik secara langsung, sehingga setiap pemanggilan menyalin isi struct ke dalam fungsi tersebut. Fungsi geser menerima struct Titik *, sehingga yang diterimanya adalah alamat memori dan perubahan yang dilakukannya menempel pada data aslinya. Kedua pola ini dideklarasikan pada program Crystal berikut.
@[Link(ldflags: "#{__DIR__}/geometri.o")]
lib Geometri
struct Titik
x : Float64
y : Float64
end
fun jarak_nol(t : Titik) : Float64
fun geser(t : Titik*, dx : Float64, dy : Float64) : Nil
end
t = Geometri::Titik.new
t.x = 3.0
t.y = 4.0
puts Geometri.jarak_nol(t)
Geometri.geser(pointerof(t), 10.0, 20.0)
puts t.x
puts t.y
puts Geometri.jarak_nol(t)# kompilasi pustaka C menjadi berkas objek
gcc -c -o geometri.o geometri.c
# jalankan program Crystal yang menautkan geometri.o
crystal run geometri.cr5.0
13.0
24.0
27.294688127912362Atribut @[Link] kali ini membawa opsi ldflags yang menyisipkan berkas objek hasil kompilasi C ke perintah link. Berkas objek seperti geometri.o adalah hasil kompilasi satu berkas C yang belum dihubungkan dengan apa pun, sehingga bentuk inilah cara paling sederhana untuk mengikutinya. Ekspresi #{__DIR__} menghasilkan direktori geometri.cr saat kompilasi, sehingga tautan tetap benar berapa pun lokasi program dijalankan. Urutan perintahnya juga penting: gcc harus selesai membuat geometri.o sebelum crystal run memanggil linker.
Struct dibuat dengan Geometri::Titik.new yang menghasilkan struct berisi nol, lalu kedua fieldnya diisi melalui setter. Pemanggilan jarak_nol menyalin struct tersebut ke C dan mengembalikan 5.0, hasil perhitungan sisi C atas salinan itu. Selanjutnya pointerof(t) mengambil alamat t bertipe Pointer(Geometri::Titik) dan meneruskannya ke geser. Fungsi C menulis melalui alamat tersebut, dan pembacaan t.x serta t.y dari sisi Crystal memperlihatkan hasilnya, yaitu 13.0 dan 24.0. Inilah alur dua arah yang dimaksud: struct dibuat di Crystal, diubah oleh C, lalu dibaca kembali di Crystal.
Struct yang dideklarasikan di dalam blok lib hidup di bawah nama bloknya, sehingga struct ini bernama lengkap Geometri::Titik dan tidak bertabrakan dengan struct lain yang mungkin bernama sama. Seperti struct Crystal pada umumnya, struct C di sisi Crystal adalah tipe nilai: penugasan dan penerusan argumen menyalin isinya. Sifat inilah yang membuat pemanggilan jarak_nol(t) terlindung dari perubahan, karena fungsi C bekerja pada salinannya sendiri, sedangkan jalur geser sengaja memilih alamat agar perubahannya menempel.
Pointer dan memori manual#
Selain alamat satu struct, Crystal juga dapat mengelola blok memori yang menampung banyak struct sekaligus. Pointer(Geometri::Titik).malloc(3) mengalokasikan ruang untuk tiga elemen, dan setiap elemen diakses melalui indeks. Metode sizeof menghitung ukuran satu elemen dalam byte, sedangkan tumpukan.value membaca elemen pertama dengan hasil yang sama seperti tumpukan[0].
lib Geometri
struct Titik
x : Float64
y : Float64
end
end
def buat(x : Float64, y : Float64) : Geometri::Titik
t = Geometri::Titik.new
t.x = x
t.y = y
t
end
puts sizeof(Geometri::Titik)
tumpukan = Pointer(Geometri::Titik).malloc(3)
tumpukan[0] = buat(3.0, 4.0)
tumpukan[1] = buat(6.0, 8.0)
tumpukan[2] = buat(5.0, 12.0)
puts tumpukan.value.x
3.times do |i|
t = tumpukan[i]
puts "Titik #{i}: #{t.x}, #{t.y}"
end# program ini tidak memanggil C, cukup dijalankan langsung
crystal run tumpukan.cr16
3.0
Titik 0: 3.0, 4.0
Titik 1: 6.0, 8.0
Titik 2: 5.0, 12.0Elemen diisi dengan menugaskan struct utuh ke tumpukan[i], dan pembacaan tumpukan.value.x mengonfirmasi isi elemen pertama dari jalur yang berbeda. Ukuran satu Titik adalah 16 byte, sesuai dua field Float64 yang berukuran 8 byte masing-masing. Perhitungan ini sederhana karena kedua field bertipe sama. Pada struct C yang fieldnya beragam, compiler C menyisipkan byte pengisi (padding) agar setiap field sejajar di memori, sehingga ukuran hasilnya dapat lebih besar daripada jumlah ukuran fieldnya. Karena itu, memeriksa sizeof di kedua sisi merupakan kebiasaan yang sehat sebelum struct bertipe campuran diteruskan.
Memori hasil Pointer.malloc dikelola pengumpul sampah (garbage collector) Crystal, sehingga program tidak membebaskannya secara manual. Perbedaannya dengan malloc C murni terletak pada pengelolaannya: memori yang dibuat pustaka C tetap menjadi tanggung jawab pustaka tersebut, sedangkan memori yang dialokasikan Crystal dipantau pengumpul sampahnya.
Kesalahan umum#
Kesalahan pertama adalah menulis field melalui elemen pointer. Penugasan tumpukan[0].x = 9.0 lolos kompilasi, tetapi perubahan itu terjadi pada salinan sementara yang dibaca dari elemen tersebut, sehingga isi memori tidak pernah berubah. Perbaikannya adalah menyusun struct utuh terlebih dahulu, lalu menugaskannya dengan tumpukan[0] = t, sebagaimana dilakukan program di atas.
Kesalahan kedua adalah memberikan struct pada parameter yang menantikan pointer. Deklarasi fun geser(t : Titik*, ...) menuntut Pointer(Geometri::Titik), sehingga pemanggilan Geometri.geser(t, 1.0, 2.0) ditolak compiler dengan pesan must be Pointer(Geometri::Titik), not Geometri::Titik. Perbaikannya adalah meneruskan pointerof(t). Arah sebaliknya berlaku pula, yaitu pointer tidak boleh diberikan pada parameter yang menantikan struct.
Kesalahan ketiga adalah menjalankan program tanpa mengompilasi pustakanya terlebih dahulu. Apabila geometri.o belum ada, error undefined reference muncul pada tahap link dan menyebut nama fungsi C yang tidak ditemukan. Perbaikannya adalah mengulang urutan perintah pada blok bash di atas, yaitu gcc -c -o geometri.o geometri.c sebelum crystal run.
Ringkasan#
- Struct C dideklarasikan di dalam blok
libdengan urutan dan tipe field yang sama dengan C, sehingga kedua sisi berbagi layout memori yang sama. - Struct diteruskan ke C sebagai salinan apabila parameternya bertipe struct, dan sebagai alamat memori apabila parameternya bertipe pointer.
pointerofmengambil alamat struct di Crystal, dan fungsi C dapat mengubah isi struct aslinya melalui alamat tersebut.Pointer.mallocmenyediakan blok memori yang dikelola pengumpul sampah Crystal, sedangkan memori dari pustaka C tetap menjadi tanggung jawab pustakanya.
Referensi#
- The Crystal Programming Language Reference — crystal-lang.org — Dokumentasi resmi bahasa dengan bagian C bindings yang membahas struct, pointer, dan penautan pustaka.
- Dokumentasi API Crystal 1.22.1 — crystal-lang.org — Rujukan pustaka standar untuk Pointer, pointerof, dan sizeof.