在軟件開發的廣闊天地里,不同編程語言各有所長。C++ 以其卓越的性能、強大的功能和對硬件的直接操控能力,在系統開發、游戲引擎、服務器等底層領域占據重要地位,但c++編寫的程序需要編譯,這往往是一個耗時操作,特別對于大型程序而言編譯可能耗費幾十分鐘;而Lua 則憑借其輕量級、可嵌入性和靈活的腳本特性,在游戲腳本、配置管理等方面大放異彩。當 C++ 與 Lua 攜手合作,它們能夠優勢互補,創造出更強大、更靈活的應用程序。本文將帶你逐步深入了解 C++ 和 Lua 聯合編程,從基礎概念到實際應用,領略這種編程方式的魅力。
一、?C++ 與 Lua 聯合編程
(一)C++ 與 Lua 的特點
C++ 是一種靜態類型的編程語言,它擁有豐富的特性,如面向對象編程、泛型編程等。這使得 C++ 在處理大規模、高性能的系統級任務時表現出色,像游戲引擎、操作系統組件等對性能和資源管理要求極高的場景,C++ 都是不二之選。
Lua 則是一種輕量級的腳本語言,它的語法簡潔,易于學習和使用。Lua 的核心優勢在于其可嵌入性,能夠輕松地被集成到其他應用程序中,為程序提供靈活的腳本功能。在游戲開發中,Lua 常被用于編寫游戲邏輯腳本、用戶界面交互腳本,以及實現游戲的熱更新功能,讓開發者無需重新編譯整個程序,就能修改游戲內容。
(二)聯合編程
將 C++ 和 Lua 聯合編程,就像是讓兩位高手相互配合。C++ 負責搭建堅實的底層框架,處理計算密集型任務和資源管理;Lua 則專注于實現靈活多變的業務邏輯和用戶交互。這種合作方式不僅能提高開發效率,還能使應用程序更具擴展性和可維護性。比如在游戲開發中,C++ 實現游戲的核心引擎功能,包括圖形渲染、物理模擬等;Lua 則用于編寫游戲角色的行為邏輯、任務腳本和關卡配置,這樣當需要修改游戲內容時,只需要更新 Lua 腳本,無需重新編譯 C++ 代碼,大大縮短了開發周期。
二、搭建聯合編程環境
(一)安裝 Lua
-
Windows 系統:從 Lua 官方網站下載 Windows 安裝包,安裝過程中記得勾選將 Lua 添加到系統環境變量。安裝完成后,打開命令提示符,輸入 “lua -v”,如果顯示 Lua 的版本信息,就說明安裝成功了。
-
Linux 系統:以 Ubuntu 為例,在終端中執行 “sudo apt-get install lua5.3” 命令,就能輕松完成安裝。安裝后,同樣可以在終端輸入 “lua -v” 來驗證是否安裝成功。或者通過官網安裝,整個安裝過程很簡單,就算遇到問題網上也能找到大量解決方法。
關于lua的安裝和基礎可以參考:Lua 從基礎入門到精通(非常詳細),本文著重于聯合編程,lua和c++基礎不會展開。
(二)準備 C++ 開發環境
-
Windows 系統:推薦使用Visual Studio,官網點開下載,配置選擇c++桌面應用
-
Linux 系統:自帶
三、C++ 與 Lua 的基礎交互
(一)第一個程序:hello lua
1.首先引入頭文件,在代碼開頭,使用?extern "C"
?是因為 C++ 支持函數重載,會對函數名進行修飾,而 Lua 是用 C 語言編寫的,其函數名沒有經過修飾。通過?extern "C"
?可以確保 C++ 編譯器以 C 語言的方式處理這些 Lua 頭文件中的函數名,避免鏈接錯誤。lua.h
?提供了 Lua 核心功能的接口,lauxlib.h
?是 Lua 輔助庫,提供了一些方便的函數,lualib.h
?則用于打開 Lua 標準庫。
extern "C"
{#include <lua.h>#include <lauxlib.h>#include <lualib.h>
}
2.main
?函數是程序的入口點。lua_open()
?函數用于創建一個新的 Lua 狀態機,它就像是一個容器,管理著 Lua 解釋器的所有狀態信息。隨后,luaopen_base(L)
、luaopen_string(L)
?和?luaopen_table(L)
?分別打開了 Lua 的基礎庫、字符串庫和表庫。這些庫提供了 Lua 編程中常用的功能,比如基礎的算術運算、字符串處理和表操作等。
int main(int argc, char* argv[])
{lua_State* L = lua_open(); luaopen_base(L);luaopen_string(L);luaopen_table(L);
3.luaL_loadfile(L, "main.lua")
?嘗試加載名為?main.lua
?的 Lua 腳本文件。如果加載過程中出現錯誤,該函數會返回一個非零值。一旦發生錯誤,程序會打印?loadfile error:
?提示信息,并通過?lua_tostring(L, -1)
?從 Lua 棧中獲取錯誤信息(這里的-1值讀取lua棧頂,應為出現異常lua會把錯誤提示信息壓入棧頂),將其打印出來,最后返回 -1 表示程序異常退出。?
if(luaL_loadfile(L,"main.lua")){printf("loadfile error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}
4.lua_pcall(L, 0, 0, 0)
?用于執行之前加載的 Lua 腳本。lua_pcall
?是一個安全的調用函數,它會捕獲 Lua 腳本執行過程中拋出的異常。第一個參數?L
?是 Lua 狀態機,第二個參數?0
?表示傳遞給 Lua 腳本的參數數量,第三個參數?0
?表示期望從 Lua 腳本獲取的返回值數量,第四個參數?0
?表示錯誤處理函數的索引。如果執行過程中出現錯誤,同樣會把錯誤信息壓入lua棧頂,通過?lua_tostring(L, -1)
?從 Lua 棧中獲取錯誤信息,最后返回 -1。
if(lua_pcall(L,0,0,0)){printf("pcall error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}
5。最后,lua_close(L)
?用于關閉 Lua 狀態機,釋放相關的資源。
lua_close(L);return 0;
}
?完整cpp文件:
extern "C"
{#include <lua.h>#include <lauxlib.h>#include <lualib.h>
}int main(int argc, char* argv[])
{lua_State* L = lua_open(); luaopen_base(L);luaopen_string(L);luaopen_table(L);if(luaL_loadfile(L,"main.lua")){printf("loadfile error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}if(lua_pcall(L,0,0,0)){printf("pcall error:\n");const char* error = lua_tostring(L,-1);printf("\t%s\n",error);return -1;}lua_close(L);return 0;
}
lua腳本(main.lua):?
print("hello lua")
(二)lua調用c++實現的函數
1.基本函數調用
先來看完整代碼
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int Ctest(lua_State* L)
{std::cout << "Cpp:Ctest" << std::endl;std::cout << lua_gettop(L) << std::endl;size_t len;const char* name = lua_tolstring(L,1,&len);int age = lua_tonumber(L,2);bool is = lua_toboolean(L,3);std::cout << lua_gettop(L) << std::endl;std::cout << "name: " << name << " age: " << age << " is: " << is << std::endl; return 0;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L,"ctest",Ctest);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson1.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_close(L);return 0;
}
lua腳本(lesson1.lua):?
ctest("xiaoming" , 13, nil)
我們定義的Ctest函數接收3個參數,字符串、數字、bool類型,代碼運行時程序會把參數依次壓入棧,壓入完畢后從棧低開始從下往上算,第一個是字符串(索引1),第二個是數字(索引2),第三個是bool數據(索引3),我們分別用lua_tolstring,lua_tonumber,lua_toboolean從棧中取出這些數據并轉換數據類型為CPP支持類型,這里的return 0指的是本函數的返回值個數是0個,也就是沒有返回值。
在完成函數定義后,我們需要將定義的函數注冊給lua腳本,使用lua_register(L,"ctest",Ctest);其中第一個參數是lua狀態機,第二個參數是lua腳本中函數名,這個函數名可以和cpp中定義的函數名不同,第三個是cpp中實現函數的函數指針。
完成上述操作后,就可以在lua腳本中調用ctest函數了。
2.array類型數據作為參數
還是先來看完整代碼
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int Ctestarr(lua_State* L)
{std::cout << "ctestarr" << std::endl;int len =luaL_getn(L,-1);std::cout << "len: " << len << std::endl;for(int i = 0; i < len ; i++){lua_pushnumber(L,i+1);lua_gettable(L,1);//pop index push table[index]size_t size;std::cout << lua_tolstring(L,-1,&size) << std::endl;lua_pop(L,1);}return 0;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L,"ctestarr",Ctestarr);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson2.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_close(L);return 0;
}
腳本(lesson2.lua):
arr = {"xiaoming", "xiaohong", "xiaogang"}
ctestarr(arr)
我們定義的Ctestarr函數接收1個array數據,代碼運行時程序會把參數壓入棧,通過luaL_getn(L,-1)來計算array的長度,他的第一個參數是lua狀態機,第二個是array在棧中位置,-1代表棧頂。獲取長度后用佛如循環遍歷讀取array元素,每次讀取先使用lua_pushnumber(L,i+1);向棧中壓入一個索引值(lua索引從1開始,而不是從0開始,所以這里是i+1),然后執行lua_gettable(L,1);這個函數第二個參數是array在棧中位置,因為代碼運行時程序會把參數壓入棧,所以array一直在棧底(前面讀取長度時用-1是因為棧內只有array,它即在棧底也在棧頂,而現在由于壓入了索引,array已經不是棧頂了),lua_gettable(L,1);執行時會先對將lua棧中索引出棧,然后壓入索引對于元素值,所以在tostring讀取后記得執行lua_pop(L,1)恢復棧空間(lua_pop()函數第二個參數是出棧個數,出棧只從棧頂出,不是位置)。
3.帶有鍵值對的table類型數據作為參數
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int CtestTable1(lua_State* L)
{std::cout << "ctesttable1" << std::endl;lua_pushnil(L);while(lua_next(L,1) != 0)//每次調用從先棧頂彈出一個值,然后push key, push value{std::cout << lua_tostring(L,-2) << ": " << lua_tostring(L,-1) << std::endl;lua_pop(L,1);}return 0;
}int CtestTable2(lua_State* L)
{std::cout << "ctesttable2" << std::endl;lua_getfield(L,1,"name");//會把value壓入棧頂std::cout << lua_tostring(L,-1) << std::endl;lua_pop(L,1);lua_getfield(L,1,"age");//會把value壓入棧頂std::cout << lua_tostring(L,-1) << std::endl;lua_pop(L,1);return 0;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L,"ctesttable1",CtestTable1);lua_register(L,"ctesttable2",CtestTable2);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson3.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;return 0;
}
arr = {name="xiaoming",age = "18", id = "22300"}
ctesttable1(arr)
ctesttable2(arr)
兩種方法可以讀取,lua_next(L,1)函數第二個參數是table在棧中位置,它在執行時會先出戰一個數據,然后入棧key,最后入棧value,也就是在第一次讀取時我們要先手動壓棧一個nil值。讀取結束后再手動出棧一個值。
第二種方法是lua_getfield(L,1,"name");第二個參數是table在棧中位置,第三個參數是要讀取的key值,執行結束會將輸入key對于的value值壓棧,讀取結束后需要手動出棧以復原占空間。
4.帶返回值的情況
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int CtestRe(lua_State* L)
{lua_pushstring(L,"return value");lua_pushnumber(L,100);lua_newtable(L);lua_pushstring(L,"key");lua_pushstring(L,"value");lua_settable(L,-3);lua_pushstring(L,"key2");lua_pushnumber(L,123);lua_settable(L,-3);return 3;
}int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);lua_register(L, "ctestre", CtestRe);std::cout << "1: " <<lua_gettop(L) << std::endl;if(luaL_loadfile(L,"lesson5.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "2: " << lua_gettop(L) << std::endl;if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;return 0;
}
a,b,c = ctestre()
print(a)
print(b)
for k,v in pairs(c) doprint(k,v)
end
我們在函數中執行lua_pushstring(L,"return value");??lua_pushnumber(L,100);向棧中壓入一個string數據,一個number數據,執行lua_newtable(L);向棧中壓入一個空table,然后一次壓棧key值和value值,再執行lua_settable(L,-3);將壓入的key和value加入table,這里的-3是之前壓入的空table的棧中位置,因為壓入了key和value,所以從占地開始從上往下第三個空間才是table,執行lua_settable后會把棧頂的key和value兩個空間出棧,此時table又回到棧頂。
5.c++向lua中設置全局變量和讀取lua中定義的全局變量
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);luaopen_string(L);luaopen_table(L);lua_pushstring(L,"value");//由c++設置全局變量lua_setglobal(L,"key");lua_pop(L,1);lua_pushnumber(L,18);//由c++設置全局變量lua_setglobal(L,"age");lua_pop(L,1);lua_newtable(L);lua_pushstring(L,"name");lua_pushstring(L,"xiaoming");lua_settable(L, -3);// 會把key,value出棧 lua_pushstring(L,"age");lua_pushnumber(L,13);lua_settable(L, -3);//還是-3lua_setglobal(L,"person");lua_pop(L,1);if(luaL_loadfile(L,"lesson6.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << "1: " <<lua_gettop(L) << std::endl;lua_getglobal(L,"width");int width = lua_tonumber(L,-1);std::cout << "1: " <<lua_gettop(L) << std::endl;lua_pop(L,1);std::cout << "1: " <<lua_gettop(L) << std::endl;std::cout << "width = " << width << std::endl;lua_getglobal(L,"table");std::cout << "2: " <<lua_gettop(L) << std::endl;lua_getfield(L,-1,"age");std::cout << "2: " <<lua_gettop(L) << std::endl;std::cout << "age = " << lua_tonumber(L,-1) << std::endl;lua_pop(L,2);std::cout << "2: " <<lua_gettop(L) << std::endl;std::cout << lua_gettop(L) << std::endl;return 0;
}
print(key)
print(age)
for k,v in pairs(person) doprint(k,v)
end
table = {name = "xiaohong" , age = "16"}
width = 100
這部分很簡單,要注意的就是棧空間的管理,并且設置全部變量的位置應該再pcall之前,讀取應該在pcall之后,要不然讀不到。
(三)c++調用lua實現的函數
1.調用函數
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);luaopen_string(L);luaopen_table(L);if(luaL_loadfile(L,"lesson7.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_getglobal(L,"event");lua_pushstring(L,"xiaoming ");std::cout << lua_gettop(L) << std::endl;if(lua_pcall(L,1,1,0) != 0){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;lua_pop(L,1);}else{std::cout << "pcall success: " << lua_tostring(L,-1) << std::endl;std::cout << lua_gettop(L) << std::endl;lua_pop(L,1);}std::cout << lua_gettop(L) << std::endl;return 0;
}
function event(a)print("event")print(a)return "lua_event"
end
lua_getglobal(L,"event");先將函數壓棧,由于函數需要一個參數,lua_pushstring(L,"xiaoming ");把參數壓棧,之后調用lua_pcall(L,1,1,0)執行,這個函數的第二個參數是"event"函數參數個數(1個),第二個是"event"函數返回值個數(1個)函數執行會把event和參數出棧,把"event"函數返回值入棧。
2.錯誤處理函數(lua_pcall第四個參數設置)
extern "C"
{#include "lua.h"#include <lualib.h>#include <lauxlib.h>
}#include <iostream>
#include <string.h>int main(int argc, char* argv[])
{lua_State* L = lua_open();luaopen_base(L);luaopen_string(L);luaopen_table(L);if(luaL_loadfile(L,"lesson8.lua")){std::cout << "load file failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}if(lua_pcall(L,0,0,0)){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout << error << std::endl;return -1;}std::cout << lua_gettop(L) << std::endl;lua_getglobal(L,"err");int err = lua_gettop(L);lua_getglobal(L,"even");//故意少寫一個t,觸發錯誤lua_pushstring(L,"xiaoming ");if(lua_pcall(L,1,1,err) != 0){std::cout << "pcall failed" << std::endl;const char* error = lua_tostring(L, -1);std::cout <<"CPP ERR: "<< error << std::endl;lua_pop(L,1);}else{std::cout << "pcall success: " << lua_tostring(L,-1) << std::endl;std::cout << lua_gettop(L) << std::endl;lua_pop(L,1);}std::cout << lua_gettop(L) << std::endl;return 0;
}
function err(e)print("LUA_ERR: "..e)return "lua_error"
endfunction event(a)print("event")print(a)return "lua_event"
end
lua_pacll第四個參數是錯誤處理函數在棧中位置,正常當不設置錯誤處理時當發生錯誤,擼啊會把錯誤提示入棧,當設置之后,lua會把錯誤提示作為參數出啊如錯誤處理函數,錯誤處理函數執行后再把其返回值(這里是“lua_err”)壓入棧。
四、聯合編程的挑戰與應對策略
(一)內存管理
在 C++ 和 Lua 聯合編程中,內存管理是一個重要問題。由于 C++ 需要手動管理內存,而 Lua 有自己的垃圾回收機制,在傳遞數據時需要特別小心。例如,當 C++ 創建一個對象并傳遞給 Lua 時,需要確保在 Lua 使用完該對象后,C++ 能夠正確地釋放內存。可以通過智能指針等方式來輔助內存管理,確保對象在不再使用時被正確釋放。
(二)異常處理
C++ 和 Lua 的異常處理機制不同,在聯合編程時需要統一處理異常,以確保程序的穩定性。可以在 C++ 中捕獲 Lua 腳本執行過程中拋出的異常,并進行適當的處理。例如,使用 “lua_pcall” 函數代替 “lua_call” 函數,“lua_pcall” 函數可以捕獲 Lua 函數執行過程中的異常,并將異常信息壓入 Lua 棧,C++ 代碼可以從棧中獲取異常信息并進行處理。
(三)性能優化
雖然 C++ 性能較高,但在與 Lua 交互時,頻繁的棧操作和數據傳遞可能會帶來性能開銷。為了優化性能,可以盡量減少不必要的棧操作,批量傳遞數據,而不是逐個傳遞。同時,對性能敏感的代碼部分,可以使用 C++ 實現,而將邏輯相對簡單、變化頻繁的部分交給 Lua 處理。
C++ 與 Lua 聯合編程為開發者提供了強大的工具,讓我們能夠充分發揮兩種語言的優勢。通過深入學習和實踐,你可以利用這種編程方式開發出更具擴展性、靈活性和高性能的應用程序。無論是游戲開發、腳本化工具還是其他領域,C++ 與 Lua 的聯合都能為你的項目帶來新的活力。