Memanggil Lua dari C

Skrip konfigurasi, callback, dan pengenalan sandboxing dari sisi host C.

Daftar isi

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.

lua konfigurasi.lua
-- 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"))
end
text Keluaran
batas_baris	50
nama_berkas	catatan.txt
mode	ringkas
preferensi("batas")	50

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

bash
# periksa versi penerjemah yang tersedia
lua -v
# jalankan uji bentuk berkas konfigurasi tanpa aplikasi host
lua konfigurasi.lua
text Keluaran
Lua 5.4.9  Copyright (C) 1994-2026 Lua.org, PUC-Rio
batas_baris	50
nama_berkas	catatan.txt
mode	ringkas
preferensi("batas")	50

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

c Contoh: memuat berkas konfigurasi
// 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.

c Contoh: membaca global dari stack
// 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.

c Contoh: memanggil fungsi Lua dengan lua_pcall
// 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.

c Contoh: mendaftarkan fungsi C untuk skrip
// 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.

c Contoh: membuka pustaka terpilih dan meniadakan io serta os
// 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.

lua Contoh: efek sandbox dari sisi skrip
-- 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)
text Keluaran
eksekusi berhasil	false
pesan menyebut os	true

Keluaran 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_dofile dan luaL_dostring mengeksekusi skrip pada host, sedangkan lua_getglobal membaca 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_register mengekspos 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#