ABI (Application Binary Interface) adalah kontrak biner yang mengatur bagaimana kode dari dua bahasa bertemu pada level mesin. Topik ini memeriksa kontrak itu pada tiga jenis data yang paling sering melintasi batas bahasa, yaitu string, struct yang dilewatkan by value, dan callback. Pustaka C kecil bernama abi.c dibangun menjadi libabi.so dengan gcc, kemudian program Odin memanggil kelima fungsinya. Keluaran aslinya ditempelkan pada bagian akhir. Tiga pelajaran intinya adalah panjang string tidak ikut melintasi batas sehingga dihitung ulang di sisi C, kepemilikan memori menentukan siapa yang wajib melepaskan blok yang dialokasikan, dan konvensi panggilan “c” menjadi syarat bagi proc Odin yang disentuh kode C.
Kontrak biner antarbahasa#
API menyatukan dua program pada level sumber, sedangkan ABI menyatukannya pada level biner. Kontrak ABI mencakup ukuran dan perataan tipe data, tata letak struct, serta konvensi panggilan (calling convention), yakni aturan tingkat mesin tentang tempat argumen diserahkan, misalnya register atau stack, beserta cara nilai kembalian diambil. Dua compiler yang mengikuti konvensi yang sama dapat saling memanggil cukup dengan nama simbol pada berkas biner masing-masing. Odin menetapkan konvensi default miliknya sendiri untuk proc biasa, sehingga proc yang berhubungan dengan C harus ditandai dengan proc "c".
Deklarasi sisi C di Odin dilakukan tanpa berkas header. Pernyataan foreign import abi { "libabi.so", } menautkan pustaka dinamis saat program dibangun, sedangkan blok foreign abi mendaftarkan setiap fungsinya dengan bentuk proc "c" yang diakhiri ---, penanda proc tanpa tubuh karena tubuhnya berada di pustaka C. Di dalam blok foreign, konvensi memang sudah “c” secara bawaan, tetapi menuliskannya secara eksplisit menjaga kontrak itu tetap terlihat. Pustaka uji berikut memuat kelima fungsi yang menjadi bahan latihan seluruh topik ini.
// tidak-dijalankan
// pustaka bersama C: dibangun dengan gcc -shared -fPIC, bukan dijalankan langsung
/* Pustaka C kecil untuk menguji ABI bersama program Odin. */
/* Panjang string dihitung dengan perulangan gaya strlen: baca sampai NUL. */
int panjang_string(const char *s) {
int n = 0;
while (s[n] != '\0') {
n = n + 1;
}
return n;
}
/* Menerima cstring dan mengembalikan versi huruf besar dalam buffer statis. */
const char *kapital(const char *s) {
static char buf[64];
int i = 0;
while (s[i] != '\0' && i < 63) {
char c = s[i];
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
buf[i] = c;
i = i + 1;
}
buf[i] = '\0';
return buf;
}
/* Titik pada bidang dengan tata letak identik terhadap struct Titik di Odin. */
typedef struct {
double x;
double y;
} Titik;
/* Ukuran struct di sisi C, untuk dibandingkan dengan size_of di sisi Odin. */
int ukuran_titik(void) {
return (int)sizeof(Titik);
}
/* Struct dilewatkan by value dan dikembalikan by value: titik tengahnya. */
Titik tengah(Titik a, Titik b) {
Titik t;
t.x = (a.x + b.x) / 2.0;
t.y = (a.y + b.y) / 2.0;
return t;
}
/* Callback: cb dipanggil untuk setiap elemen, hasilnya ditulis balik. */
void terapkan(int *arr, int n, int (*cb)(int)) {
for (int i = 0; i < n; i = i + 1) {
arr[i] = cb(arr[i]);
}
}# bangun pustaka C menjadi libabi.so dari dalam folder topik
gcc -shared -fPIC abi.c -o libabi.soPerintah gcc memakai dua opsi yang keduanya wajib untuk pustaka dinamis. Opsi -fPIC menghasilkan position independent code, yaitu kode yang dapat diletakkan pada alamat memori mana pun saat pustaka dimuat, sedangkan opsi -shared menetapkan keluaran berupa berkas .so yang dimuat saat program berjalan alih-alih disalin ke dalam program. Berkas libabi.so diletakkan di folder topik karena deklarasi foreign import menunjuk nama berkas itu apa adanya.
String: panjang di Odin, NUL di C#
String Odin menyimpan sepasang nilai, yaitu pointer ke data dan panjangnya, sehingga len dibaca tanpa memindai memori. Sebaliknya, cstring pada C hanya menyimpan pointer, dan akhir string ditandai dengan byte NUL yang bernilai 0. Karena panjang tidak ikut melintasi batas, fungsi panjang_string pada abi.c menghitung ulang panjang dengan perulangan gaya strlen, yakni membaca karakter satu per satu sampai menemui '\0'.
// tidak-dijalankan
cs_lit := cstring("Halo") // literal: NUL dijamin compiler
cs_aman := strings.unsafe_string_to_cstring(sapaan) // transmute tanpa alokasi
cs_baru := strings.clone_to_cstring(pesan) // salinan baru ber-NUL
defer delete(cs_baru) // alokasi dibalas pelepasanAda tiga jalur menuju cstring, dan masing-masing menetapkan kepemilikan yang berbeda. Cast cstring("...") hanya diterima compiler ketika operandnya string literal karena data literal memang diakhiri NUL, sedangkan cast yang sama terhadap variabel bertipe string ditolak dengan pesan Cannot cast ... from 'string'. Proc strings.unsafe_string_to_cstring melakukan transmute tanpa alokasi, sehingga hanya aman bila NUL sudah pasti berada di akhir data. Proc strings.clone_to_cstring menyalin isi string lalu menambahkan NUL melalui alokasi baru, dan alokasi itu wajib dibalas delete. Jalur sebaliknya memakai cast string(cs): cstring yang dikembalikan C dipindai sampai NUL untuk memperoleh string Odin dengan panjang yang tepat, tanpa penyalinan sama sekali.
Pada program utama, literal “Halo dari Odin” dikirim lewat unsafe_string_to_cstring karena NUL-nya terjamin, pesan hasil fmt.tprintf dikirim lewat clone_to_cstring lalu dilepaskan dengan delete, dan cstring yang dikembalikan kapital dibaca kembali menjadi string Odin. Versi huruf besar disimpan pada buffer statis, yakni blok memori kerja milik pustaka C, sehingga string hasil cast tidak memiliki memori sendiri dan tidak boleh dilepaskan dari sisi Odin. Ketiga jalur itu tampak berjajar pada keluaran program di bagian akhir.
Struct dengan tata letak identik#
Dua struct dapat dipertukarkan lintas bahasa hanya bila tata letaknya identik byte demi byte, bukan sekadar mirip secara bentuk. Struct Titik di Odin mendeklarasikan dua field f64, dan typedef pada abi.c mendeklarasikan dua field double dengan urutan yang sama. Tipe double C dan f64 Odin sama-sama lebar 64 bit, urutan field menetapkan offset masing-masing, dan tidak ada field tambahan di kedua sisi, sehingga kedua struct menempati 16 byte dengan offset 0 dan 8. Kecocokan itu tidak diasumsikan, melainkan diuji langsung: size_of(Titik) di Odin dan fungsi ukuran_titik di C sama-sama melaporkan 16.
// tidak-dijalankan
// sisi C pada abi.c: typedef struct { double x; double y; } Titik;
Titik :: struct {
x, y: f64,
}
foreign abi {
tengah :: proc "c" (a, b: Titik) -> Titik ---
ukuran_titik :: proc "c" () -> c.int ---
}Fungsi tengah menerima dua struct dan mengembalikan satu struct, semuanya dilewatkan by value. Artinya, setiap argumen disalin ke register atau stack sesuai konvensi “c”, dan struct kembalian disalin balik ke pemanggil. Tidak ada pointer yang terlibat, tidak ada alokasi, dan tidak ada memori yang perlu dilepaskan, sehingga kepemilikan tidak menjadi persoalan pada jalur ini. Penyalinan by value menjadi mahal pada struct berukuran besar, dan pola yang lazim adalah melewatkan pointer ke struct, misalnya ^Titik di Odin yang berpasangan dengan struct Titik * di C. Pointer pada kedua bahasa hanyalah alamat 8 byte, sehingga parameter arr bertipe ^c.int pada fungsi terapkan memakai prinsip yang sama dengan int * milik C.
Callback: proc “c” sebagai pointer fungsi#
Callback adalah fungsi yang dikirim sebagai argumen agar dapat dipanggil oleh pihak lain, dan pada C callback direpresentasikan sebagai pointer fungsi, misalnya int (*)(int) pada prototipe terapkan. Deklarasi foreign memodelkan parameter itu sebagai tipe proc berkonvensi “c”, yaitu cb: proc "c" (x: c.int) -> c.int. Penanda konvensi wajib karena tipe proc tanpa penanda memakai konvensi “odin” yang menyertakan context implisit, dan konvensi tersebut berbeda dari konvensi C pada level ABI meskipun nama fungsi dan jumlah argumennya tampak sama.
Sisi Odin menyediakan callback berupa proc biasa yang ditandai proc "c" dan bertanda tangan sama persis dengan pointer fungsi di C. Pada program ini, callback tersebut bernama gandakan_dua dan mengembalikan x * 2. Fungsi terapkan pada abi.c memanggil callback itu untuk setiap elemen array dan menulis balik hasilnya, sehingga proses transformasi array berlangsung sepenuhnya di dalam kode C sementara logika transformasinya ditulis di Odin. Setiap proc “c” dengan tanda tangan yang sama dapat menggantikan gandakan_dua tanpa mengubah satu baris pun di abi.c, dan fleksibilitas itu bersumber dari kontrak biner yang ditetapkan pada deklarasinya.
// tidak-dijalankan
foreign abi {
// cb bertipe pointer fungsi C: int (*)(int)
terapkan :: proc "c" (arr: ^c.int, n: c.int, cb: proc "c" (x: c.int) -> c.int) ---
}
gandakan_dua :: proc "c" (x: c.int) -> c.int {
return x * 2
}Menjalankan program lengkap#
Program utama menguji ketiga jenis data dalam satu kali eksekusi. Deklarasi seluruh fungsi C terkumpul pada satu blok foreign, struct Titik dideklarasikan berdampingan dengan typedef C-nya, dan callback Odin ditulis sebagai proc biasa yang ditandai konvensi “c”. Fungsi main membagi pengujian menjadi tiga bagian berurutan, yaitu string, struct, dan callback. Jalankan perintah berikut dari dalam folder topik setelah libabi.so selesai dibangun.
package main
import "core:c"
import "core:fmt"
import "core:strings"
// pustaka C yang sudah dibangun menjadi libabi.so di folder topik
foreign import abi {
"libabi.so",
}
foreign abi {
panjang_string :: proc "c" (s: cstring) -> c.int ---
kapital :: proc "c" (s: cstring) -> cstring ---
ukuran_titik :: proc "c" () -> c.int ---
tengah :: proc "c" (a, b: Titik) -> Titik ---
terapkan :: proc "c" (arr: ^c.int, n: c.int, cb: proc "c" (x: c.int) -> c.int) ---
}
// tata letak struct harus identik dengan typedef Titik pada abi.c
Titik :: struct {
x, y: f64,
}
// proc "c" dengan tanda tangan sama persis terhadap int (*)(int)
gandakan_dua :: proc "c" (x: c.int) -> c.int {
return x * 2
}
main :: proc() {
// string literal: compiler menjamin NUL di akhir memori belakangnya
sapaan := "Halo dari Odin"
fmt.println("len Odin :", len(sapaan))
fmt.println("panjang C:", panjang_string(strings.unsafe_string_to_cstring(sapaan)))
// string hasil runtime: tidak ada jaminan NUL, salin dengan NUL tambahan
pesan := fmt.tprintf("suhu %.1f derajat", 27.5)
cs, err := strings.clone_to_cstring(pesan)
fmt.println("clone err:", err)
fmt.println("panjang C:", panjang_string(cs))
delete(cs) // milik Odin, dilepaskan Odin
// cstring dari C: dibaca sebagai string Odin tanpa penyalinan
balik := string(kapital(cstring("odin bahasa sistem")))
fmt.println("kapital C:", balik, "len:", len(balik))
// struct: ukuran diuji di kedua sisi, lalu dilewatkan by value
fmt.println("ukuran Odin:", size_of(Titik), "ukuran C:", ukuran_titik())
p := Titik{x = 3, y = 4}
q := Titik{x = 7, y = 1}
t := tengah(p, q)
fmt.printfln("titik tengah: (%v, %v)", t.x, t.y)
// callback: proc "c" Odin dipanggil oleh C untuk setiap elemen array
nilai := [5]c.int{1, 2, 3, 4, 5}
fmt.println("sebelum :", nilai[:])
terapkan(&nilai[0], c.int(len(nilai)), gandakan_dua)
fmt.println("sesudah :", nilai[:])
}# sumberkan lingkungan sandbox jalur odin bila belum tersedia
[ -f /home/kal/my-project/tools/env.sh ] && source /home/kal/my-project/tools/env.sh
# jalankan program dari dalam folder topik
odin run .len Odin : 14
panjang C: 14
clone err: None
panjang C: 17
kapital C: ODIN BAHASA SISTEM len: 18
ukuran Odin: 16 ukuran C: 16
titik tengah: (5, 2.5)
sebelum : [1, 2, 3, 4, 5]
sesudah : [2, 4, 6, 8, 10]Setiap baris keluaran menegaskan satu klaim yang dipaparkan di atas. Dua baris pertama menunjukkan bahwa len Odin dan perulangan C menghasilkan angka yang sama, yaitu 14, untuk literal yang sama. Baris clone err: None menandakan alokasi salinan berhasil, dan baris panjang C: 17 mengonfirmasi string runtime 17 karakter yang ber-NUL terbaca utuh oleh C. Baris kapital C membuktikan cstring milik C dibaca kembali menjadi string Odin dengan len 18. Pasangan ukuran Odin: 16 ukuran C: 16 menutup uji tata letak, sedangkan pasangan sebelum dan sesudah memperlihatkan callback C memanggil proc Odin untuk menggandakan setiap elemen array.
Kesalahan umum#
Kesalahan pertama adalah mengirim string runtime melalui jalur yang keliru. Compiler menolak cast cstring(variabel) pada setiap string biasa, sedangkan strings.unsafe_string_to_cstring pada string runtime bisa saja kebetulan berhasil, misalnya ketika byte 0 berada tepat setelah blok data. Perbaikannya hanya satu, yaitu menyalin dengan strings.clone_to_cstring dan menutupnya dengan defer delete(cs) tepat setelah alokasi, sehingga fungsi C selalu menerima data ber-NUL yang utuh.
Kesalahan kedua adalah salah menentukan target delete. String hasil cast string(cs) atas cstring milik C tidak memiliki memori sendiri, sehingga memanggil delete atasnya berarti melepaskan buffer statis pustaka C yang tidak pernah dialokasikan oleh Odin, dan perilakunya tak terdefinisi. Arah yang berlawanan sama berbahaya, yaitu lupa delete atas hasil strings.clone_to_cstring sehingga salinannya bocor. Aturan penentuannya satu kalimat, yaitu pihak yang mengalokasikan dialah yang melepaskan.
Kesalahan ketiga adalah callback tanpa penanda konvensi. Proc salah_konvensi :: proc (x: c.int) -> c.int tanpa penanda “c” ditolak compiler saat diserahkan ke terapkan dengan pesan Expected "cdecl", got "odin". Perbaikannya dengan menandai proc callback sebagai proc "c" serta memastikan tanda tangan persis sama dengan deklarasinya di blok foreign, termasuk tipe parameter dan tipe kembaliannya.
Ringkasan#
- String Odin menyimpan pointer beserta panjang, sedangkan
cstringC hanya pointer yang diakhiri NUL, sehingga panjang dihitung ulang di sisi C dengan perulangan gayastrlen. - Jalur konversi yang aman: cast
cstring("...")hanya untuk literal,strings.unsafe_string_to_cstringuntuk string yang NUL-nya terjamin,strings.clone_to_cstringuntuk string runtime disertaidelete, dan caststring(cs)untuk membaca cstring milik C tanpa penyalinan. - Struct lintas bahasa menuntut tata letak byte yang identik, diuji dengan
size_of(Titik)di Odin dansizeofdi C, sedangkan pemanggilan by value berarti penyalinan penuh tanpa kewajiban memori. - Callback C adalah pointer fungsi yang dipasangkan dengan proc “c” Odin bertanda tangan sama, dan ketidakcocokan konvensi panggilan ditolak compiler sebelum program berjalan.
Referensi#
- Overview — Odin Documentation — Panduan resmi bahasa yang memuat foreign declarations, calling conventions, serta string dan cstring.
- Paket core:strings — Referensi paket Odin
— Dokumentasi
clone_to_cstringdanunsafe_string_to_cstringyang dipakai pada konversi string. - Paket core:c — Referensi paket Odin
— Definisi tipe C seperti
c.intdanc.doubleyang dipakai pada deklarasi foreign.