Template 的魅力之一在於自動產生程式碼,大幅減少冗餘作業。 C++14的 integer_sequence 與 17 的 fold expression,是屬於兩個用法較不直覺的新功能。我想就 Lua 在 C++ 的整合來作說明。
使用 C 或遊戲業的人或多或少都聽過 Lua,甚至自己就是使用者。對不了解 Lua 的人,簡單地說 Lua 是一門以 C 實作的直譯語言,執行時相當於運轉 virtual machine。Lua 與 C 的互動,便是透過對 virtual machine 的 stack 進行操作,達成 C 呼叫 Lua function、Lua 呼叫 C function 的概念。
如此這個實作就有一個顯而易見的難處:
// https://www.lua.org/pil/26.1.html
static int l_sin (lua_State *L) {
double d = lua_tonumber(L, 1);
lua_pushnumber(L, sin(d));
return 1;
}
儘管這個例子看來單純,但針對不同數量參數、不同返回型別的函數,程式設計師就必須重新撰寫類似的片段。我們如何利用 C++ 的技巧來解決這個問題?首先,最大的難題「我們要怎麼知道一個函數的signature」那便是利用 template argument deduction 的技巧。
template <typename _signature>
struct function;
template <typename _ret, typename... _args>
struct function<_ret(_args...)> {
using result_type = _ret;
using parameter_types = std::tuple<_args...>;
static constexpr size_t parameter_count = sizeof...(_args);
using type = _ret (*)(_args...);
};
在此使用 tuple 有兩個用處,第一是為了將展開的 parameter pack 保存為 struct 的資訊之一,第二是為了在 Lua 呼叫 C function 時將引數傳入 function 內。
template <typename _args, size_t... _indexes>
inline void pop_all_args(lua_State* handle,
_args& args,
std::index_sequence<_indexes...>) {
(pop_arg(std::get<_indexes>(args), handle, _indexes + 1), ...);
}
template <typename _function>
struct lua_bind {
static int invoke(lua_State* handle) {
typename _function::parameter_types args;
constexpr auto indexes =
std::make_index_sequence<_function::parameter_count>{};
pop_all_args(handle, args, indexes);
// ...
}
};
index_sequence(integer_sequence) 的技巧同樣是利用編譯器推導時,讓 _indexes 成為一串整數序列。fold expression 便能發揮它的作用,我們來看一個範例:
inline void pop_all_args(lua_State* handle,
std::tuple<char, int, float>& args,
std::integer_sequence<std::size_t, 0, 1, 2>) {
pop_arg(std::get<0>(args), handle, 0 + 1),
pop_arg(std::get<1>(args), handle, 1 + 1),
pop_arg(std::get<2>(args), handle, 2 + 1);
}
當我們寫到 pop_arg 事情便簡單了,因為 C++ 提供我們 overloading 與 template 的能力,我們只要這麼寫就能處理多種型別。
template <typename _arg>
inline auto pop_arg(_arg& arg, lua_State* handle, size_t i)
-> std::enable_if_t<std::is_integral_v<_arg>, void> {
arg = lua_tointeger(handle, i);
}
template <typename _arg>
inline auto pop_arg(_arg& arg, lua_State* handle, size_t i)
-> std::enable_if_t<std::is_floating_point_v<_arg>, void> {
arg = lua_tonumber(handle, i);
}
inline auto pop_arg(const char*& arg, lua_State* handle, size_t i) {
arg = lua_tostring(handle, i);
}
使用這些技巧讓我們生成與 Lua 溝通函數的過程十分簡單,在編譯時期也能達成型別安全。我透過這樣的方法,完成了簡單的 C binding 與 Lua binding;由於 Lua 能達成多值返回,除此還有很多機能,但 binding 的實現大同小異。這邊留下幾個問題給有興趣自己實作的讀者作挑戰:
- 文章中解釋了怎麼實作 Lua binding,那麼要如何實作 C binding
- 如何實作多值返回,並同時兼容
tuple、void、builtin多種 return type 的寫法,例如:void()、int(int)、tuple<int>(int)、tuple<int, int>(int)