“Howling at the Moon: Lua for C++ Programmers” 听起来像是一篇或一次分享,旨在让熟悉 C++ 的开发者迅速理解并上手 Lua。它可能关注以下几个核心方面:
概念 | C++ | Lua |
---|---|---|
变量 | 强类型,例如 int , float 。 |
动态类型,一个变量可以存 1 , "foo" , {} 。 |
函数 | 支持重载、模板等复杂特性。 | 一切都是函数(第一类对象),无重载。 |
面向对象 | class/struct, 继承, 虚函数。 | 使用表(table)和元表(metatable)模拟 OOP。 |
内存管理 | malloc/free 或 C++ new/delete。 | 由垃圾回收器自动管理。 |
模块系统 | .h/.cpp + make /CMake。 |
require 加载 .lua 文件脚本模块。 |
luaL_newstate()
, luaL_openlibs()
创建 VM;luaL_dofile()
运行脚本;lua_getglobal()
, lua_pushnumber()
, lua_pcall()
等接口。lua.new_usertype<Player>("Player",
"new", sol::constructors<Player(std::string)>(),
"move", &Player::move
);
lua.script(R"(
p = Player.new("Alice")
p:move(10, 20)
)");
-- 变量与函数定义
local x = 42
function greet(name)
print("Hello, "..name.."!")
end
-- 使用元表模拟一个类
Point = {}
Point.__index = Point
function Point.new(x,y)
return setmetatable({x=x,y=y}, Point)
end
function Point:move(dx, dy)
self.x = self.x + dx
self.y = self.y + dy
end
-- 创建并调用
p = Point.new(1, 2)
p:move(3, 4)
“Howling at the Moon” 恐怕是用一句轻松的方式告诉程序员:像“对 Lua 狂吠”那样将它嵌入 C++ 项目,让你既保留 C++ 性能,又享受脚本语言的灵活。适合用在游戏、实时系统、工具链中的插件体系等场景。
如你对如何具体用 Sol2、如何设计 Lua API 或者脚本调试机制有兴趣,我可以继续帮你深入!
;
空语句(无操作)varlist = explist
变量赋值语句functioncall
函数调用label
标签(跳转标识)break
跳出循环goto Name
跳转到标签do block end
局部代码块while exp do block end
while 循环repeat block until exp
repeat-until 循环if exp then block {elseif exp then block} [else block] end
条件语句for Name = exp, exp [, exp] do block end
数值for循环for namelist in explist do block end
泛型for循环function funcname funcbody
函数定义local function Name funcbody
局部函数定义local namelist [= explist]
局部变量定义return [explist] [;]
返回零个或多个表达式,可选分号结束。::Name::
用于 goto 语句跳转。.
和:
操作符的复合名字。这段内容其实就是Lua语言的正式语法定义(BNF风格)和语言简要说明。它表明:
print("Hello World!")
非常直接,类似很多脚本语言。function f(a1, a2, a3)
-- body
end
等同于:f = function(a1, a2, a3)
-- body
end
std::function
或lambda表达式**,但语法更简洁且直接作为语言核心部分。print("Vanilla print") -- 调用原始的 print 函数,输出 "Vanilla print"
print = function(...)
-- 这里写你自己的打印实现,比如 my_print
-- [...]
end;
print("My print") -- 现在调用的是替换后的 print 函数
print
是一个全局变量,指向一个函数。print
赋值一个新的匿名函数,从而覆盖原来的 print
实现。print(...)
都会执行新赋的函数,而不是默认的打印行为。print("Hello") -- 输出 Hello
print = function(...)
local args = {...}
for i,v in ipairs(args) do
io.write("MyPrint: ", tostring(v), " ")
end
io.write("\n")
end
print("Hello") -- 输出 MyPrint: Hello
总结:Lua 中函数是动态可替换的变量,随时能用新函数覆盖旧函数,赋予极大灵活性。
print
函数被调用的次数。具体理解如下:count = 0; -- 定义计数器变量,初始化为0
old_print = print; -- 保存原始的 print 函数到 old_print
print = function(...) -- 用匿名函数替换全局的 print
count = count + 1; -- 每次调用 print,计数器加1
old_print(...); -- 调用原始的 print 函数,保证功能正常
end;
count
old_print
print
,所以先保存原始 print
函数,方便后续调用。print
函数print
替换成一个新的函数,这个新函数会先增加计数器,然后再调用原始 print
。print
仍然调用旧的 print
,保证打印行为和之前完全一样,只是在背后多做了计数。print("Hello") -- count = 1
print("World") -- count = 2
print("Total print calls:", count) -- 输出次数统计
总结:这段代码展示了如何利用 Lua 的函数动态替换功能给 print
添加计数功能,同时保持原功能不变,是一种经典的函数钩子(hook)示例。
function enable_counting()
local count = 0; -- 局部变量,计数器,初始为0
local old_print = print; -- 保存原始 print 函数
print = function(...) -- 用新的函数替换全局 print
count = count + 1; -- 每次调用增加计数
old_print(...); -- 调用原始 print 保持功能
end;
return function() return count; end; -- 返回一个匿名函数,访问计数器 count
end
count
是局部变量enable_counting
函数内部定义,不会污染全局环境。print
函数print
函数能访问并修改 count
,这就是闭包的关键:函数“捕获”了外层作用域的变量 count
。enable_counting
返回了一个函数,这个函数内部访问了同一个 count
变量,能够读取当前的调用次数。count
,Lua 通过词法作用域(lexical scoping)自动捕获了 count
,形成闭包。local get_count = enable_counting()
print("Hello")
print("World")
print("Print called", get_count(), "times") -- 输出调用次数
local array = { 5, 4, 3, 2, 1 };
assert(array[2] == 4); -- 注意:Lua数组下标是从1开始的,不是0
{ 5,4,3,2,1 }
是一个数组初始化。array[2]
是4。local dict = { the_answer = 42 };
assert(dict["the_answer"] == 42);
the_answer
作为键,42作为值。dict.the_answer
语法访问。dict[print] = "function as key";
print
作为键,存储字符串值。local list = { value = "foo", next = nil };
list.next = { value = "bar", next = nil };
list.next
并不会复制数据,而是改变了引用指向。local complex = { real = 42.0, imag = 0.0 };
complex
的 table,表示复数,有两个字段:real
和 imag
。complex["real"] = 42.0;
complex.real = 42.0;
complex["real"]
或 complex.real
两种语法访问和赋值字段。.
是 []
的简写,只能用于合法的标识符。function fconjugate(c)
c.imag = 0.0 - c.imag;
end
complex.conjugate = fconjugate;
fconjugate
,它接受一个复数 c
,计算共轭复数(虚部取负)。complex
的字段 conjugate
,相当于给复数对象添加了一个“方法”。complex.conjugate(complex); -- 直接调用,显式传入 self(complex)
complex:conjugate(); -- 冒号语法,隐式传入 self(complex)
complex.conjugate(complex)
是标准函数调用,需要自己传入 complex
作为参数。complex:conjugate()
是 Lua 的“方法调用语法”,等价于 complex.conjugate(complex)
,自动将调用对象作为第一个参数传入。obj:method()
) 是一种语法糖,会隐式把表自身作为第一个参数传递给函数。struct Complex {
double real;
double imag;
void conjugate() { imag = -imag; }
};
Complex c{42.0, 0.0};
c.conjugate();
Lua 代码就是用 table + 函数模拟了类似的功能。
sum = c1 + c2; -- ???
这种无法直接工作的情况。function build_complex(r, i)
return { real = r, imag = i };
end
build_complex
是一个工厂函数,返回一个带有 real
和 imag
字段的表(即“复数对象”)。local c1 = build_complex(1, 0);
local c2 = build_complex(0, 1);
c1
和 c2
都是“复数”对象,分别表示 1 + 0i
和 0 + 1i
。local sum = c1 + c2; -- ???
+
运算。+
运算,需要给表设置一个元表(metatable),并实现 __add
元方法。function build_complex(r, i)
local c = { real = r, imag = i }
setmetatable(c, {
__add = function(a, b)
return build_complex(a.real + b.real, a.imag + b.imag)
end
})
return c
end
local c1 = build_complex(1, 0)
local c2 = build_complex(0, 1)
local sum = c1 + c2
print(sum.real, sum.imag) -- 输出 1 1
setmetatable
给每个复数对象设置了元表,并定义了 __add
,使得用 +
运算时会调用该函数。+
操作符。__add
元方法来“重载”加法。+
进行复数相加。+
。local mt = {};
mt
,用于存储对象的特殊行为。mt.__add = function(c1, c2)
return build_complex(c1.real + c2.real, c1.imag + c2.imag);
end;
mt
设置 __add
元方法,它定义了两个复数对象相加时的行为。c1 + c2
时,Lua 会调用这里的函数。function build_complex(r, i)
local ret = {real = r, imag = i};
setmetatable(ret, mt);
return ret;
end
build_complex
是一个工厂函数,创建一个具有 real
和 imag
字段的表。setmetatable(ret, mt)
,给新表绑定元表 mt
,这样新对象就能支持 +
操作。__add
元方法定义了两个复数对象用 +
运算时的行为。c1 + c2
就变成调用元表中的函数,实现复数的相加。local a = build_complex(1, 2)
local b = build_complex(3, 4)
local c = a + b
print(c.real, c.imag) -- 输出: 4 6
这样,你就可以用自然的算术表达式处理自定义复数对象啦。
function build_date(y, m, d)
assert(validDate(y, m, d))
build_date
是一个工厂函数,用来构建“日期”对象。validDate
函数检查传入的年月日是否合法,如果不合法则断言失败(程序终止)。 local lself = { y=y, m=m, d=d }
lself
保存实际的日期数据(年、月、日)。lself
是个表,存放日期的内部状态。 local lget_day = function() return lself.d end
lget_day
,用来获取日期中的“日”字段。lself
中的 d
。 local lset_day = function(nd)
assert(validDate(lself.y, lself.m, nd))
lself.d = nd
end
lset_day
,用来设置新的“日”值 nd
。validDate
验证新的日期是否合法,保证数据有效性。 return {
set_day = lset_day,
get_day = lget_day
}
end
set_day
和 get_day
。lself
,实现了封装。lself
是局部变量,外部无法直接访问,保护了内部状态。set_day
和 get_day
。local date = build_date(2023, 6, 20)
print(date.get_day()) -- 20
date.set_day(25)
print(date.get_day()) -- 25
date.set_day(32) -- 如果32不是合法日期,会触发断言失败
这就是Lua中用闭包和局部变量实现简单面向对象封装的经典写法。
function is_complex(c)
return type(c) == "table" and c.real and c.imag;
end
is_complex
是一个类型检测函数,用来判断传入的变量 c
是否是一个复数对象。c
是一个表(type(c) == "table"
),real
和字段 imag
,这两个字段通常用来表示复数的实部和虚部。c
是一个复数结构。local tuple = {};
for k,v in pairs(t) do
tuple[#tuple + 1] = v;
end
t
中抽取所有的值,收集到数组 tuple
中。pairs(t)
遍历表 t
的所有键值对 (k,v)
。tuple[#tuple + 1] = v
作用是将 v
按顺序追加到数组 tuple
末尾,#tuple
是当前数组长度。tuple
是 t
所有字段值的一个有序集合(数组形式)。假设有个复数对象:
local c = { real = 1.0, imag = 2.0 }
print(is_complex(c)) -- true
local t = {a=10, b=20, c=30}
local tuple = {}
for k,v in pairs(t) do
tuple[#tuple+1] = v
end
-- tuple可能是 {10,20,30} 顺序不保证,因为pairs遍历无序
for k in pairs(_G) do
print(k);
end
_G
是 Lua 的 全局环境表(global environment table),它存储了所有的全局变量和全局函数。pairs(_G)
是遍历 _G
表中的所有键(即所有全局变量的名字)。for k in pairs(_G) do print(k) end
这段代码会打印当前 Lua 环境中所有定义的全局变量名。_G
表的字段:Lua 中所有的全局变量本质上都是 _G
这个表的键值对。_G["foo"] = 123 -- 定义全局变量 foo = 123
print(foo) -- 输出 123
_G
可以列出当前所有的全局变量和函数名,有助于调试或动态访问。foo = 10
bar = "hello"
for k in pairs(_G) do
print(k)
end
可能会打印:
foo
bar
print
math
table
...
这里会列出自定义的全局变量 foo
、bar
,以及 Lua 默认提供的标准库名字(print
、math
、table
等)。
_G
是 Lua 的全局变量环境表。_G
表的字段。_G
可以列出所有全局变量名。local foobar; -- 局部变量声明
foobar = do_stuff(); -- 可能对全局变量foobar赋值,如果上面写成local,则这里不会影响全局
-- 重要提醒:
-- _G只是一个普通的表
setmetatable(_G, {
__newindex = function(_, name)
print(name .. " was not declared!");
end
});
_G
是全局环境表,本质上是一个普通的Lua表。setmetatable(_G, {...})
给这个全局表设置元表。__newindex
元方法会在向表中写入一个新键时被触发。__newindex
函数在向 _G
里新增一个字段(即创建一个全局变量)时,打印警告信息 name .. " was not declared!"
。foo = 1
,这会自动在全局表 _G
中创建变量 foo
。这在大型项目中容易出错,写错变量名或者未声明就使用。_G
设置 __newindex
元方法,可以拦截这些新变量的创建操作,从而打印警告或执行其它逻辑。local foobar;
是声明一个局部变量,这样后面赋值不会触发全局变量创建。setmetatable(_G, {
__newindex = function(_, name)
print(name .. " was not declared!");
end
});
foo = 123
执行结果:
foo was not declared!
说明:foo
没有事先声明是局部变量,就直接被赋值成了全局变量,于是触发警告。
_G
。_G
设置 __newindex
元方法,可以监控和限制全局变量的创建。local
声明局部变量。int main() {
// 1. 创建一个新的Lua状态机(Lua虚拟机实例)
lua_State* l = luaL_newstate();
// 2. 通过API运行一段Lua代码字符串
luaL_dostring(l, R"(
print("Hello World!");
)");
// 3. 关闭Lua状态机,释放资源
lua_close(l);
}
lua_State* l
是Lua虚拟机的上下文对象,所有Lua API调用都通过它操作Lua环境。luaL_newstate()
:初始化一个新的Lua虚拟机实例。luaL_dostring(l, "...")
:直接执行传入的Lua代码字符串。lua_close(l)
:关闭虚拟机,释放所有内存。lua_
开头的函数是Lua核心API,负责Lua状态机管理、栈操作等底层交互。luaL_
是Lua辅助库API,封装了常用功能,比如加载和运行脚本字符串,简化了核心API的使用。luaL_newstate
、luaL_dostring
和 lua_close
是启动、运行和关闭Lua的常用函数。lua_
是核心API,luaL_
是辅助库API,后者更方便调用。int my_function(lua_State* l) {
// [...]
}
my_function
是一个符合Lua C API调用约定的函数。lua_State
的指针,代表当前Lua虚拟机状态。int
,表示该C函数向Lua返回的结果数量(即返回给Lua的值的个数)。int func(lua_State* l)
签名。lua_tonumber(l, 1)
获取第1个参数,lua_pushnumber(l, result)
把结果压入栈。int
告诉Lua调用者,有多少返回值被压入了栈。int my_function(lua_State* l) {
// 1. 从Lua栈读取参数,例如第1个参数是数字
double arg = lua_tonumber(l, 1);
// 2. 处理逻辑,比如简单地乘以2
double result = arg * 2;
// 3. 把结果压回Lua栈
lua_pushnumber(l, result);
// 4. 返回结果数量(这里是1)
return 1;
}
然后通过Lua注册API把这个函数暴露给Lua:
lua_register(l, "my_function", my_function);
Lua脚本就可以直接调用my_function
:
print(my_function(5)) -- 输出10
int my_function(lua_State* L) {
// 从栈的第1个位置读取参数
double x = lua_tonumber(L, 1);
double y = lua_tonumber(L, 2);
// 计算结果
double result = x + y;
// 把结果压回栈
lua_pushnumber(L, result);
// 返回结果数量
return 1;
}
// 推送一个数字类型到Lua栈
void push(lua_State* l, lua_Number n) {
lua_pushnumber(l, n);
}
// 推送一个字符串到Lua栈
void push(lua_State* l, char const* s) {
lua_pushstring(l, s);
}
// 使用C++17折叠表达式,把多个参数依次压入Lua栈
template<typename... Ts>
void pushargs(lua_State* l, Ts... args) {
(push(l, args), ...);
}
push(lua_State* l, lua_Number n)
lua_pushnumber
,将数字n
压入Lua栈顶。lua_Number
通常是double
类型,代表Lua中的数字。push(lua_State* l, char const* s)
lua_pushstring
,将C字符串压入Lua栈顶。char const* s
,而实现里用的是str
(应为s
,代码可能有个笔误)。pushargs
(push(l, args), ...)
,依次调用上面定义的push
函数,逐个把每个参数压入Lua栈。pushargs(l, 3.14, "hello", 42)
会依次压数字3.14、字符串"hello"、数字42到栈顶。push
函数,为不同类型提供了统一接口,简化调用代码。pushargs
让你用一个函数就能将多个不同类型的参数依次压栈,方便调用Lua函数时传递多个参数。push(lua_State* l, char const* s)
中lua_pushstring(l, str);
中的str
应该改为s
,否则编译错误。template<typename... Ts>
void pushargs(lua_State* l, Ts... args) {
auto t = boost::hana::make_tuple(args...);
boost::hana::for_each(t,
[l](auto&& v) { push(l, v); });
}
boost::hana::make_tuple(args...)
args...
放进一个Hana元组。boost::hana::for_each(t, [...])
t
的每个元素,执行给定的lambda。[l](auto&& v) { push(l, v); }
l
,对元组每个元素v
调用push(l, v)
压栈。template<typename... Ts>
void pushargs(lua_State* l, Ts... args) {
auto t = std::make_tuple(args...);
push_helper(l, t,
std::make_index_sequence<sizeof...(Ts)>());
}
template<typename... Ts, std::size_t... Is>
void push_helper(lua_State* l,
std::tuple<Ts...> const& t,
std::index_sequence<Is...>) {
using expander = int[];
(void) expander{ 0, (push(l, std::get<Is>(t)), 0)... };
}
std::make_tuple(args...)
std::tuple
。std::make_index_sequence()
0,1,...,N-1
,用于索引元组元素。push_helper
函数std::get(t)
,调用push(l, ...)
压栈。(void) expander{ 0, (push(l, std::get(t)), 0)... };
push
依次被调用。方案 | 依赖 | 实现方式 | 优点 | 缺点 |
---|---|---|---|---|
Boost.Hana 元组 + for_each | 需要Boost.Hana | 利用Hana元编程函数遍历元组 | 代码简洁,表达力强 | 依赖外部库 |
std::tuple + 索引序列 | 标准库 | 使用索引序列访问元组展开参数 | 无外部依赖,跨平台 | 语法稍复杂,稍不直观 |
这两种方法都让你能传入任意数量和类型的参数,把它们一一压入Lua栈,方便调用Lua函数时传递参数。
idx
个元素读取出来,转成对应的C++类型。??? getValueFromStack(lua_State* l, int idx) {
switch(lua_type(l, idx)) {
case LUA_TNUMBER:
return Number(lua_tonumber(l, idx));
case LUA_TSTRING:
return String(lua_tostring(l, idx));
// [...]
}
}
lua_State* l
int idx
lua_type(l, idx)
idx
位置值的类型,返回一个整型标识,比如LUA_TNUMBER
,LUA_TSTRING
等。switch
根据类型进行分支处理:
LUA_TNUMBER
lua_tonumber(l, idx)
把它转成C的double
(或者C++的Number
封装类)。LUA_TSTRING
lua_tostring(l, idx)
取出C字符串指针,再包装成String
类(假设用户自定义的字符串类型)。// [...]
LUA_TBOOLEAN
、表LUA_TTABLE
、函数LUA_TFUNCTION
等。???
标示,通常会用一个多态封装类型或者std::variant
等来支持多种Lua数据类型。std::variant<double, std::string, bool> getValueFromStack(lua_State* l, int idx) {
switch(lua_type(l, idx)) {
case LUA_TNUMBER:
return lua_tonumber(l, idx);
case LUA_TSTRING:
return std::string(lua_tostring(l, idx));
case LUA_TBOOLEAN:
return (bool)lua_toboolean(l, idx);
default:
throw std::runtime_error("Unsupported Lua type");
}
}
std::variant
**来统一表示多种不同类型的值(Lua值的C++映射),并实现根据值类型获取其“类型标识”的方法。enum class Type; // 定义一个枚举类,表示所有支持的类型,比如 Number、String 等
class Number {
public:
Type type() const; // 返回 Number 类型标识
};
class String {
public:
Type type() const; // 返回 String 类型标识
};
// 其他类型类似定义...
using Value = std::variant<Number, String /*, 其他类型... */>;
// Value 可以存 Number 或 String 等类型
Type getType(Value const& v) {
return std::visit(
[](auto x) { return x.type(); },
v);
}
enum class Type
enum class Type {
Number,
String,
// ...
};
用来表示不同类型的统一标识。class Number
和 class String
type()
,返回自己对应的Type
枚举值(如Type::Number
或Type::String
)。using Value = std::variant
std::variant
实现类型安全的联合体,Value变量可以持有Number
或String
(及其他类型)中的任何一个。Type getType(Value const& v)
std::visit
访问Value
当前存储的那个类型的对象,然后调用该对象的type()
方法,获取它的类型标识。std::variant
是实现动态类型安全的好工具,既避免了类型不安全的void*
,又避免手写复杂的类型管理代码。type()
方法给每个类型提供类型标识,使得程序在处理Value
时,能知道当前持有的数据类型是什么,方便做动态分支或匹配。假设:
enum class Type { Number, String };
class Number {
public:
Type type() const { return Type::Number; }
double val;
};
class String {
public:
Type type() const { return Type::String; }
std::string val;
};
using Value = std::variant<Number, String>;
Type getType(Value const& v) {
return std::visit([](auto&& x) { return x.type(); }, v);
}
然后:
Value v1 = Number{3.14};
Value v2 = String{"hello"};
assert(getType(v1) == Type::Number);
assert(getType(v2) == Type::String);
总结:这段代码用现代C++优雅地表示了动态语言的多种数据类型,并实现类型检测,非常适合Lua这类语言的C++绑定实现。
call
,用来从 C++ 调用 Lua 函数,传递任意数量和类型的参数,并返回 Lua 函数的多个返回值。template<typename... Ts>
std::vector<Value> call(
lua_State* l,
char const* func,
Ts... args)
{
lua_getglobal(l, func); // 把全局 Lua 函数压栈
pushargs(l, args...); // 把所有参数逐个压栈
lua_call(l, sizeof...(Ts), LUA_MULTRET, 0); // 调用 Lua 函数
return getValuesFromStack(l); // 从 Lua 栈中获取所有返回值,封装为 std::vector
}
Ts...
lua_getglobal(l, func)
func
的 Lua 函数压入 Lua 栈顶。pushargs(l, args...)
push
函数,把参数压入 Lua 栈。lua_call(l, sizeof...(Ts), LUA_MULTRET, 0)
sizeof...(Ts)
,告诉 Lua 我们要获取所有返回值(LUA_MULTRET
)。getValuesFromStack(l)
Value
类型,放进 std::vector
返回。call(l, "print", 42, "Hello World");
这句代码调用 Lua 中的 print
函数,传递了两个参数 42
和 "Hello World"
,Lua端会打印:
42
Hello World
函数本身也会返回所有 Lua 函数的返回值(print
通常返回nil
,不过如果是自定义函数,可以返回值)。
call
函数模板的“限制”和“重载”设计,以及对 Lua 语言的总结。template<typename... Ts>
std::vector<Value> call(lua_State*, char const*, Ts...);
std::array<Value, 2> call(lua_State*, char const*, Value, Value);
std::tuple<Number, String> call(lua_State*, char const*, Number, Number);
std::vector
,适合不确定参数和返回值数量的情况。Value
参数,返回一个包含两个 Value
的 std::array
。Number
参数,返回包含 Number
和 String
的 std::tuple
。Lua is a powerful, efficient, lightweight, embeddable scripting language.
这是对 Lua 语言的总结: