Topik ini menguraikan pola pemanggilan Lua dari sisi aplikasi host C, mulai dari mengeksekusi berkas konfigurasi, membaca nilai hasilnya ke variabel C, hingga memanggil fungsi Lua dengan penanganan error. Bahasan ditutup dengan registrasi fungsi C sebagai callback, yaitu fungsi milik host yang dapat dipanggil skrip, serta pengenalan sandboxing pada penerjemah. Semua potongan C pada topik ini memerlukan kepala pustaka Lua dan pustaka Lua dari distribusi resminya, misalnya dikompilasi dengan gcc host.c -llua -o host.
Berkas konfigurasi yang digunakan contoh#
Contoh sepanjang topik ini menggunakan satu berkas konfigurasi yang sama. Berkas tersebut menetapkan tiga nilai global dan satu fungsi yang dapat dipanggil host, sehingga ketiga pola pemanggilan dapat menunjuk padanya. Skrip konfigurasi tetap berupa kode Lua biasa, sehingga bentuknya dapat diperiksa dengan menjalankannya langsung pada penerjemah. Nilai yang dideklarasikan berupa global biasa, dan table dapat digunakan bila konfigurasinya bersarang, sehingga bentuknya tidak berbeda dari kode Lua lain.
-- berkas konfigurasi yang dimuat aplikasi host
-- host membaca nilai-nilai berikut melalui stack pada API C
batas_baris = 50
nama_berkas = "catatan.txt"
mode = "ringkas"
-- fungsi yang dapat dipanggil host pada saat berjalan
preferensi = function(opsi)
if opsi == "batas" then
return batas_baris
end
return mode
end
-- blok uji bentuk: berjalan hanya bila berkas dieksekusi langsung
-- dengan "lua konfigurasi.lua", tanpa aplikasi host
if not host then
print("batas_baris", batas_baris)
print("nama_berkas", nama_berkas)
print("mode", mode)
print("preferensi(\"batas\")", preferensi("batas"))
endbatas_baris 50
nama_berkas catatan.txt
mode ringkas
preferensi("batas") 50Keluaran di atas dihasilkan oleh blok uji bentuk pada bagian akhir berkas. Pemeriksaan if not host membuat blok uji hanya berjalan ketika berkas dieksekusi langsung tanpa aplikasi host, sedangkan host yang memuat berkas melalui luaL_dofile membaca nilai globalnya secara diam-diam. Cara menjalankan pemeriksaan bentuk dari terminal ditunjukkan berikut ini, disertai pemeriksaan versi penerjemah yang digunakan.
# periksa versi penerjemah yang tersedia
lua -v
# jalankan uji bentuk berkas konfigurasi tanpa aplikasi host
lua konfigurasi.luaLua 5.4.9 Copyright (C) 1994-2026 Lua.org, PUC-Rio
batas_baris 50
nama_berkas catatan.txt
mode ringkas
preferensi("batas") 50Menjalankan dan membaca konfigurasi#
Pola pertama mengeksekusi berkas konfigurasi pada penerjemah yang sudah ada. Fungsi luaL_dofile memuat dan mengeksekusi berkas dalam satu panggilan, sedangkan luaL_dostring melakukan hal yang sama terhadap potongan kode berbentuk string. Keduanya mengembalikan LUA_OK apabila eksekusi berhasil dan menempatkan pesan error di puncak stack apabila gagal.
// tidak-dijalankan
/* jalankan berkas konfigurasi dan tangani kegagalan pemuatannya */
if (luaL_dofile(L, "konfigurasi.lua") != LUA_OK) {
fprintf(stderr, "gagal memuat konfigurasi: %s\n", lua_tostring(L, -1));
lua_pop(L, 1); /* lepas pesan error dari stack */
return 1;
}Potongan ini berjalan pada penerjemah L yang sudah dibuat dengan luaL_newstate dan pustakanya sudah dibuka. Apabila berkas konfigurasi memunculkan error, misalnya karena sintaksnya rusak, luaL_dofile mengembalikan nilai selain LUA_OK dan pesan error berada di puncak stack, sehingga host dapat mencatat pesan tersebut sebelum berhenti. Pola pemuatan yang gagal dengan rapi ini menjadi dasar bagi seluruh pola berikutnya.
Pola kedua membaca nilai konfigurasi ke variabel C. Nilai yang ditetapkan skrip berbentuk global Lua, sehingga pembacaannya mengikuti pola stack yang sama.
// tidak-dijalankan
/* baca global batas_baris sebagai bilangan bulat */
lua_getglobal(L, "batas_baris");
int batas = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
/* baca global nama_berkas sebagai string */
lua_getglobal(L, "nama_berkas");
const char *nama = lua_tostring(L, -1);
printf("batas = %d, berkas = %s\n", batas, nama);
lua_pop(L, 1);lua_getglobal mendorong salinan nilai global yang diminta, lua_tointeger dan lua_tostring mengubah nilai teratas menjadi tipe C, dan lua_pop membereskan stack setiap kali satu nilai selesai dibaca. String yang dibaca tetap sah selama nilainya berada di stack, sehingga string itu digunakan terlebih dahulu sebelum lua_pop melepasnya. Apabila berkas konfigurasi di atas digunakan, variabel batas berisi 50 dan nama berisi teks “catatan.txt”.
Memanggil fungsi yang ditetapkan skrip#
Pola ketiga memanggil fungsi yang ditetapkan skrip. Urutannya selalu sama, yaitu mendorong fungsi ke stack, mendorong argumennya, memanggil lua_pcall, lalu membaca hasilnya dari puncak stack. Fungsi lua_pcall menerima jumlah argumen dan jumlah nilai balik yang diharapkan, serta menempatkan pesan error di puncak stack alih-alih melempar error keluar dari kode C.
// tidak-dijalankan
/* siapkan pemanggilan fungsi preferensi("batas") */
lua_getglobal(L, "preferensi"); /* dorong fungsi yang dipanggil */
lua_pushstring(L, "batas"); /* dorong argumen pertama */
/* panggil dengan 1 argumen, harap 1 hasil, tanpa fungsi penangan pesan */
if (lua_pcall(L, 1, 1, 0) != LUA_OK) {
fprintf(stderr, "error dari skrip: %s\n", lua_tostring(L, -1));
lua_pop(L, 1);
return 1;
}
/* hasil pemanggilan berada di puncak stack */
int hasil = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
printf("hasil = %d\n", hasil);Pada contoh konfigurasi, pemanggilan preferensi("batas") mengembalikan 50, yang dibaca dari puncak stack setelah lua_pcall berakhir. Apabila fungsi yang dipanggil memunculkan error, lua_pcall mengembalikan nilai selain LUA_OK dan pesan errornya berada di puncak stack, sehingga host dapat mencatatnya dan terus berjalan. Perlindungan ini menjadi alasan lua_pcall lebih disukai daripada lua_call pada batas antara host dan skrip.
Callback arah sebaliknya#
Arah integrasi sebaliknya memungkinkan skrip memanggil kemampuan host. Fungsi C yang akan dipanggil dari Lua diberi bentuk tertentu, yaitu menerima satu objek lua_State dan mengembalikan jumlah nilai yang diserahkan kepada Lua sebagai hasil. Fungsi lua_register mendaftarkan fungsi tersebut pada sebuah nama global, sehingga skrip dapat memanggilnya sebagaimana fungsi Lua biasa.
// tidak-dijalankan
/* bentuk fungsi C yang dapat dipanggil dari Lua */
static int tulis_log(lua_State *L) {
const char *pesan = luaL_checkstring(L, 1);
printf("[log] %s\n", pesan);
return 0; /* jumlah nilai balik yang diserahkan kepada Lua */
}
/* daftarkan fungsi tersebut sebagai global bernama log */
lua_register(L, "log", tulis_log);Setelah registrasi, skrip yang dimuat host dapat memanggil log("peristiwa") dan program C mencetak baris lognya. Fungsi host yang terdaftar dapat menerima argumen dan mengembalikan nilai sebagaimana fungsi Lua, karena protokolnya sama, yaitu argumen berada di stack dan hasilnya didorong sebelum kembali. luaL_checkstring pada contoh memeriksa tipe argumen pertama dan memunculkan error bila tipe tidak sesuai, sehingga kesalahan pemanggilan dari skrip terdeteksi dengan pesan yang jelas. Pola inilah yang mendasari API domain pada aplikasi host, misalnya fungsi menggambar pada mesin gim atau perintah penyuntingan pada editor, dan registrasinya dilakukan sekali sebelum skrip dimuat.
Membatasi penerjemah dengan sandbox#
Host menentukan kemampuan yang tersedia bagi skrip. Cara pertama adalah tidak membuka seluruh pustaka bawaan, melainkan memilih pustaka yang benar-benar diperlukan dengan luaL_requiref. Cara kedua meniadakan global yang berisiko, misalnya io dan os, sehingga skrip yang menjangkau keduanya menerima nil. Kedua cara dapat disertai lua_gc, yang mengatur pengelolaan memori penerjemah, apabila kebijakan host menuntut batasan tambahan.
// tidak-dijalankan
/* buka hanya pustaka yang diperlukan skrip */
luaL_requiref(L, "string", luaopen_string, 1);
luaL_requiref(L, "table", luaopen_table, 1);
luaL_requiref(L, "math", luaopen_math, 1);
lua_pop(L, 3); /* lepas nilai pustaka yang tersisa di stack */
/* nonaktifkan pustaka berisiko dengan mengisi nil pada globalnya */
lua_pushnil(L);
lua_setglobal(L, "io");
lua_pushnil(L);
lua_setglobal(L, "os");Efek pembatasan tersebut dapat diperlihatkan dari sisi skrip. Apabila global os telah diisi nil oleh host, setiap penjangkauan os.time menimbulkan error yang menunjuk global tersebut. Blok berikut meniru keadaan itu dan memeriksa pesan errornya melalui pcall.
-- meniru lingkungan sandbox: global os diisi nil oleh host
os = nil
local ok, pesan = pcall(function() return os.time() end)
print("eksekusi berhasil", ok)
print("pesan menyebut os", pesan:find("os", 1, true) ~= nil)eksekusi berhasil false
pesan menyebut os trueKeluaran menunjukkan bahwa penjangkauan gagal dan pesan errornya menunjuk global os, sehingga skrip tidak dapat melanggar batasan secara diam-diam. Pembatasan semacam ini bukan sekadar latihan. Redis menyematkan penerjemah Lua 5.1 untuk mengeksekusi skrip atomik yang dikirim klien melalui perintah EVAL, dan skrip tersebut berjalan dalam lingkungan terbatas dengan pustaka terpilih serta tanpa akses berkas dan jam sistem. Dokumentasi EVAL menyebut batasan lingkungannya secara eksplisit, sehingga praktik Redis menjadi contoh sandbox yang nyata dan ketat.
Ringkasan#
luaL_dofiledanluaL_dostringmengeksekusi skrip pada host, sedangkanlua_getglobalmembaca nilai konfigurasi ke variabel C melalui stack.- Pemanggilan fungsi mengikuti urutan dorong fungsi, dorong argumen,
lua_pcall, lalu baca hasil, dengan error tertangkap sebagai nilai balik. lua_registermengekspos fungsi C kepada skrip, sehingga integrasi dapat berjalan dua arah.- Sandboxing dibentuk dengan membuka pustaka terpilih atau meniadakan global berisiko, sebagaimana dipraktikkan Redis pada penerjemah Lua 5.1 yang tersemat.
Referensi#
- Manual Referensi Lua 5.4 — Lua.org
— Bagian The Application Program Interface mendokumentasikan
luaL_dofile,lua_pcall,lua_register, danluaL_requiref. - Programming in Lua, edisi pertama — Lua.org — Bab 25 Extending Your Program memperagakan penggunaan Lua sebagai berkas konfigurasi dari program C.
- Pengantar Evaluasi Skrip Lua — Redis — Dokumentasi resmi skrip atomik yang berjalan dalam lingkungan sandbox pada server Redis.