当前位置: 首页 > news >正文

CppCon 2018 学习:Scripting at the Speed of Thought Using Lua in C++ with sol3

“Scripting at the Speed of Thought Using Lua in C++ with sol3” 是指在 C++ 程序中利用 sol3 库(实际上是第三版,仍叫 sol2,但有些人会称为 sol3)将 Lua 脚本语言无缝集成,实现 快速开发、热重载和灵活扩展 的编程模式。这个短语强调了使用 Lua 的“快速迭代”优势,以及 sol3 带来的极简绑定方式。

一、为什么用 Lua + C++?

C++ 非常强大但编译慢,不适合频繁修改和调试逻辑代码。而 Lua:

  • 是轻量级的解释型语言;
  • 天生设计为嵌入型脚本语言;
  • 非常适合写游戏逻辑、配置、AI 脚本等。
    将 Lua 脚本嵌入 C++ 程序可以实现:
  • 快速修改脚本而无需重新编译整个项目;
  • 将核心性能代码保留在 C++ 中;
  • 使非程序员(如游戏设计师)也能通过 Lua 修改逻辑。

二、什么是 sol3?

sol3(即 sol2 的第三代)是一个现代 C++17/20 编写的、用于绑定 Lua 的头文件库。
优点包括:

  • 使用极简、几乎零样板;
  • 性能接近原生 Lua C API;
  • 支持双向绑定(从 C++ 调 Lua,也可在 Lua 调用 C++ 对象);
  • 智能管理生命周期,避免内存泄漏;
  • 支持类型推断、自动转换、异常安全。

三、简单示例:用 sol3 调用 Lua 脚本

#include <sol/sol.hpp>
#include <iostream>
int main() {sol::state lua;lua.open_libraries(sol::lib::base);// 加载 Lua 脚本lua.script(R"(function greet(name)return "Hello, " .. nameend)");// 调用 Lua 函数std::string result = lua["greet"]("world");std::cout << result << std::endl; // 输出:Hello, worldreturn 0;
}

四、将 C++ 函数/类暴露给 Lua

#include <sol/sol.hpp>
int add(int a, int b) {return a + b;
}
struct Player {std::string name;int hp;void say_hello() {std::cout << "Hi, I'm " << name << " with HP: " << hp << "\n";}
};
int main() {sol::state lua;lua.open_libraries(sol::lib::base);// 绑定函数lua.set_function("add", add);// 绑定类lua.new_usertype<Player>("Player","name", &Player::name,"hp", &Player::hp,"say_hello", &Player::say_hello);lua.script(R"(print(add(3, 4)) -- 输出 7p = Player.new()p.name = "LuaKnight"p.hp = 100p:say_hello() -- 调用 C++ 方法)");
}

五、应用场景:快速开发 + 热重载

  1. 快速修改逻辑,不需编译
    用 Lua 写游戏行为、事件、对话、技能等。
  2. 热重载脚本
    游戏运行中检测 Lua 文件变化,自动 reload() 脚本。
  3. 数据驱动设计
    游戏配置如怪物属性、关卡参数都可以是 Lua 表。

六、总结:“Scripting at the Speed of Thought”

这句话体现了 sol3 + Lua 的精髓:

  • 用 C++ 保证底层性能;
  • 用 Lua 编写高层逻辑、AI、配置;
  • 开发者可以“想到就能写、写完就能运行”,大大提升开发效率。

关于在应用中使用脚本语言(以 Lua 为例)进行扩展开发的动机与价值,我来逐段解释和扩展,以帮助你彻底理解其含义与意义:

Scripting 的核心理念

“Treating code as data” —— 把代码当作数据对待

这意味着你可以在运行时加载、执行、修改代码,就像处理数据一样。这是脚本语言的核心特性,和静态编译型语言(如 C++)最大的区别。

“Extending your application beyond ship time”

即使程序已经发布,你仍可以通过脚本来:

  • 添加新功能;
  • 修复逻辑错误;
  • 修改行为逻辑;
  • 自定义用户体验。
    不必重新编译主程序,也不必重新打包更新。

“Delegate smaller, simpler tasks to easy-to-use language”

让脚本语言(如 Lua)来处理简单、频繁变动的任务,例如:

  • UI 逻辑
  • 游戏关卡配置
  • AI 行为树
  • 数据验证
    这样主语言(C++)可以专注于性能关键部分(如渲染、物理、网络等)。

Why Script?(为什么使用脚本)

Extending without recompilation / without DLLs

  • 不需要重新编译整个 C++ 项目(节省时间)
  • 不需要制作易碎的 DLL/so 动态库(平台依赖麻烦多)

Lowering the barrier to entry

  • 许多脚本语言(Lua/Python)学习门槛低
  • 不需要复杂的开发环境
  • 逻辑清晰、语法简单
    | 对比 | C++ | Lua |
    | ---- | ------------- | --------- |
    | 构建系统 | CMake / IDE复杂 | 无需构建,直接运行 |
    | 类型系统 | 静态强类型,复杂 | 动态弱类型,灵活 |
    | 编译时间 | 慢 | 零 |
    | 学习曲线 | 陡峭 | 友好 |

“What about the Standard?”(C++ 标准库支持脚本吗?)

  • C++ 标准库中并不支持脚本语言绑定或运行时执行代码
  • 没有标准化的动态语言嵌入接口
  • 所以我们才需要 第三方库(如 sol2/sol3luabridgechaiscriptpybind11 等)

Lua, the Scripting Language(Lua 是什么)

Lua 特点:

  • 简单:语法一页纸就能概括
  • 轻量:整个语言引擎不到500KB
  • 数据结构统一:只有一个表(table)——数组 + 字典
  • 快:解释器写得非常高效

Lua 应用领域(广泛使用):

  • 游戏:《魔兽世界》的 UI 全部是 Lua 脚本写的
  • **数据库:**Redis 支持 Lua 脚本操作事务和批处理
  • **嵌入式系统:**Waze GPS 系统界面
  • 服务器组件、驱动系统、插件框架

小结

这部分内容在讲什么?整体意思可以这样概括:

C++ 程序天生强大但开发效率低,嵌入 Lua 脚本可以 让代码更易修改、更具扩展性、更加动态灵活

通过使用像 sol3 这样的绑定库,你可以轻松地在 C++ 中调用 Lua,或者让 Lua 调用 C++ 函数,把两者的优势结合起来。

这段内容深入介绍了 Lua C API 的工作方式 —— 即在 C(或 C++)中如何与 Lua 进行交互,包括 调用函数、访问表、获取返回值等操作。下面我会详细拆解这段内容,帮你理解 Lua C API 的 栈模型、调用流程 以及复合表达式的处理。

基础理解:Lua 虚拟机(VM)和 Lua C API

Lua VM 特点:

  • 用纯 ANSI C 实现,无需 C++、平台无关、可嵌入任意主程序
  • 生成的虚拟机大小仅几百 KB,非常适合嵌入式系统或游戏

Lua C API 是什么?

Lua 在 C 中提供一套 堆栈操作的函数接口,你通过它来:

  • 向 Lua 传参
  • 调用 Lua 函数
  • 获取 Lua 返回值
  • 操作 Lua 表和变量

Lua C API 是 “栈结构” 操作的

Lua C API 的操作以“栈”为核心。你操作的其实是 Lua VM 的一个“值栈”:

Top ↑   <- 栈顶(最近 push 的值)
...
Bottom ↓ <- 栈底

你通过:

  • lua_push...() 压栈
  • lua_get...() 访问表字段并压栈
  • lua_call()lua_pcall() 进行函数调用
  • lua_to...() 从栈顶获取结果并转换为 C 类型

示例:访问表字段

Lua 表达式:

my_table["a"]

在 Lua C API 中:

  1. lua_getglobal(L, "my_table"); // 栈顶是 my_table 表
  2. lua_getfield(L, -1, "a"); // my_table[“a”],压栈
  3. lua_tostring(L, -1); // 取出值为 string 类型

注意:Lua 的“索引”通常从栈顶向下数(-1 是栈顶,-2 是其下一个)

示例:函数调用

Lua 表达式:

my_func(2)

Lua C API:

lua_getglobal(L, "my_func");   // push 函数
lua_pushnumber(L, 2);          // push 参数
lua_call(L, 1, 1);             // 调用函数:1参数,1返回值
int result = lua_tointeger(L, -1); // 取返回值
lua_pop(L, 1);                 // 弹栈清理

更复杂的组合调用:

other_func( my_table["a"]["b"], my_func(2) )

分步骤解释:

1. 先处理 my_table["a"]["b"]
lua_getglobal(L, "my_table");          // -> 栈1: my_table
lua_getfield(L, -1, "a");              // -> 栈2: my_table["a"]
lua_getfield(L, -1, "b");              // -> 栈3: my_table["a"]["b"]
// 弹掉中间层以清理栈,或保留引用
2. 然后处理 my_func(2)
lua_getglobal(L, "my_func");           // -> 栈4: my_func
lua_pushnumber(L, 2);                  // -> 栈5: 2
lua_call(L, 1, 1);                     // 调用 my_func(2) => 返回值压栈
3. 最后调用 other_func(...)
lua_getglobal(L, "other_func");        // -> 栈6: other_func
// 注意:参数要先压入,再压函数
lua_insert(L, -3);  // 把函数移动到参数前面
// 现在栈是: other_func, arg1, arg2
lua_call(L, 2, 1);   // 2 个参数,1 个返回值

总结图示:

表达式:
other_func( my_table["a"]["b"], my_func(2) )
栈操作流程(从底向上):
+-----------------------------+
| return value of other_func |
+-----------------------------+
| my_table["a"]["b"]         |
+-----------------------------+
| return value of my_func(2) |
+-----------------------------+
| other_func function        |
+-----------------------------+

小结(你的笔记内容意义)

笔记解释
Entire VM is tiny and ANSI CLua 可以嵌入任何平台、编译器
Exposes Lua C API提供栈式接口与 Lua 通信
Stack-based一切操作基于值栈:push、call、pop
Lua_to{x}从栈顶取值并转为 C 类型
Complex expressions可以逐步构建复杂 Lua 表达式并调用

你这里提到的内容,主要是在讲 C 语言本身的限制,以及在调用 Lua C API 时,C 语言函数重载(overloading)缺失带来的命名混乱和使用难度。我帮你详细拆解:

1. C语言没有函数重载(Overloading)

  • 函数重载:指同一个函数名可以根据参数类型或数量不同,调用不同版本的函数(这是 C++ 的特性)。
  • C语言中没有函数重载,意味着:
    • 每个函数必须有唯一的名字。
    • 不能用同一个名字写多个参数类型不同的函数。

2. 这在 Lua C API 中的体现

Lua C API 涉及大量对 Lua 表操作的函数,但没有重载,所以很多名字很像、功能稍有区别的函数:

函数名作用说明备注
lua_gettable(L, index)根据栈上的 key,获取表中对应的 valuekey 在栈顶,弹出 key,压入 value
lua_getglobal(L, const char* name)获取全局变量(全局表 _G 的字段)类似 lua_getfield 的简写
lua_getfield(L, index, const char* key)从栈上指定位置的表中,获取字符串键对应的值不弹出 key
lua_geti(L, index, lua_Integer n)获取表的整数索引字段(Lua 5.3+)访问数组部分
lua_rawgeti(L, index, lua_Integer n)获取表的整数索引字段,不触发元方法直接访问,不会调用 __index
lua_rawget(L, index)使用栈顶 key 直接访问表,绕过元方法更底层,通常更快
lua_rawgetp(L, index, void* p)使用指针作为 key 直接访问表(元方法也绕过)通常用于 C 指针索引表

3. 造成的问题

  • 函数名字类似但参数差异大,使用时容易混淆。
  • 需要开发者记住不同函数的参数和语义。
  • C 语言无法用函数重载写一个统一名字的接口来简化调用。
  • 这就让 API 在实际用法上不够优雅。

4. 解决方案

这也是为什么 sol2/sol3 这类现代 C++ Lua 绑定库流行,因为它们用 C++ 的重载、模板和元编程技术,封装了这些底层 C API,给你提供更简洁、类型安全且易用的接口。
例如:

sol::state lua;
lua["my_table"]["key"] = 42;  // 自动封装了底层复杂的 lua_getfield、lua_setfield 等

总结

  • C 语言没有函数重载,导致 Lua C API 的接口看起来名字繁多、相似,调用时需要记忆各种细节
  • 不同函数其实都是对表操作的变体:索引类型(字符串 key / 整数 key / 指针 key)、是否触发元方法等区别。
  • 现代绑定库通过 C++ 特性大幅简化这些操作。

准备以下开发环境:

1. 安装 Lua 开发库

Windows

  • 可以从 LuaBinaries 下载预编译的 Lua 5.3/5.4 库(包含 .lib.dll
  • 或者用 MSYS2、vcpkg 等包管理器安装 Lua 开发包

Linux (Ubuntu/Debian)

sudo apt-get update
sudo apt-get install liblua5.3-dev

macOS (Homebrew)

brew install lua

2. 编译示例程序

示例代码保存为 example.cpp

编译命令示例

Linux/macOS
g++ example.cpp -o example -llua5.3 -ldl -lm

-llua5.3 连接 Lua 库,具体名称可能是 -llua,根据系统安装情况调整

-ldl 是 Linux 下动态库依赖

-lm 是数学库

Windows (MSVC)
  • 配置项目,包含 Lua 头文件目录和库目录
  • 链接 lua.lib,运行时需要 lua.dll

3. 配置包含目录和库目录

  • 包含目录:指向 Lua 的 include 文件夹,里面有 lua.h, lauxlib.h, lualib.h
  • 库目录:指向 Lua 的 .lib.a 库文件所在路径

4. 运行程序

确保 Lua 动态库(如 lua53.dll / liblua.so)在系统路径或可执行文件同目录,否则会报找不到 DLL 的错误。

5. 其他建议

  • 使用 C++17 或更高标准,方便未来用 sol3 等现代库
  • 若你用 VSCode、CLion、Visual Studio 等 IDE,可以配置编译器路径和 Lua 库,方便调试

下面是一个简单的 CMakeLists.txt 示例,帮你编译前面那个 Lua C API 的示例程序 example.cpp。它会:

  • 查找系统中的 Lua 库和头文件(Lua 5.3 或 5.4)
  • 配置编译选项
  • 链接 Lua 库和必要的系统库
cmake_minimum_required(VERSION 3.10)
project(LuaCExample)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 找 Lua 库和头文件
find_package(Lua 5.3 REQUIRED) # 如果你用的是 5.4,可以改成 5.4 或 find_package(Lua REQUIRED)
add_executable(example example.cpp)
target_include_directories(example PRIVATE ${LUA_INCLUDE_DIR})
target_link_libraries(example PRIVATE ${LUA_LIBRARIES})
# Linux 上可能需要链接 dl 和 m 库
if(UNIX AND NOT APPLE)target_link_libraries(example PRIVATE dl m)
endif()

使用方法

  1. 把你的示例代码保存为 example.cpp,跟 CMakeLists.txt 放在同一个目录。
  2. 在该目录打开终端,运行:
mkdir build
cd build
cmake ..
cmake --build .
  1. 编译成功后,会生成 example 可执行文件,运行它即可。
    如果你的系统找不到 Lua,可能需要:
  • 手动设置 LUA_INCLUDE_DIRLUA_LIBRARIES,例如:
cmake -DLUA_INCLUDE_DIR=/usr/include/lua5.3 -DLUA_LIBRARIES=/usr/lib/x86_64-linux-gnu/liblua5.3.so ..
extern "C" {
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
#include <iostream>
// 一个空指针作为 lua_rawgetp 的 key 示例
static int dummy_key;
int main() {// 创建新的 Lua 状态机lua_State* L = luaL_newstate();// 打开所有标准库luaL_openlibs(L);// 在 Lua 中创建一个复杂的表const char* script = R"(my_table = {a = { b = 42 },  -- 字段 "a" 是一个表,里面有 "b" = 4210, 20, 30,      -- 数组部分,索引1=10, 2=20, 3=30})";// 执行 Lua 脚本if (luaL_dostring(L, script) != LUA_OK) {std::cerr << "Failed to run Lua script: " << lua_tostring(L, -1) << "\n";lua_close(L);return 1;}// 1. lua_getglobal 获取全局变量 my_table,压入栈顶lua_getglobal(L, "my_table");  // 栈: my_table// 2. lua_getfield 从表中获取字段 "a",压入栈顶lua_getfield(L, -1, "a");      // 栈: my_table, my_table["a"]// 3. lua_getfield 从 my_table["a"] 获取字段 "b",压入栈顶lua_getfield(L, -1, "b");      // 栈: my_table, my_table["a"], my_table["a"]["b"]// 4. 读取栈顶的整数值int b = lua_tointeger(L, -1);std::cout << "my_table['a']['b'] = " << b << "\n";// 弹出 my_table["a"]["b"] 和 my_table["a"],恢复栈顶为 my_tablelua_pop(L, 2);  // 栈: my_table// 5. lua_geti 从表中取数组元素,Lua索引从1开始,这里取第二个元素lua_geti(L, -1, 2);  // 栈: my_table, my_table[2]int val = lua_tointeger(L, -1);std::cout << "my_table[2] = " << val << "\n";lua_pop(L, 1);  // 弹出 my_table[2],栈: my_table// 6. lua_rawgeti 访问数组元素,绕过元方法,这里取第三个元素lua_rawgeti(L, -1, 3);  // 栈: my_table, my_table[3]val = lua_tointeger(L, -1);std::cout << "my_table[3] (rawgeti) = " << val << "\n";lua_pop(L, 1);  // 弹出 my_table[3],栈: my_table// 7. lua_rawget 使用栈顶 key 直接访问表字段,这里取字段 "a"lua_pushstring(L, "a");  // 栈: my_table, "a"lua_rawget(L, -2);       // 根据键 "a" 直接访问 my_table["a"],栈: my_table, my_table["a"]if (lua_istable(L, -1)) {std::cout << "my_table['a'] is a table\n";}lua_pop(L, 1);  // 弹出 my_table["a"],栈: my_table// 8. lua_rawgetp 用 C 指针作为 key 来访问表// 先向表里插入一个以指针为键的键值对lua_pushlightuserdata(L, (void*)&dummy_key);  // 压入指针 keylua_pushstring(L, "pointer_value");           // 压入字符串 valuelua_settable(L, -3);                           // 设置 my_table[dummy_key] = "pointer_value"// 读取刚才设置的以指针为键的值lua_pushlightuserdata(L, (void*)&dummy_key);  // 压入指针 keylua_rawgetp(L, -2, (void*)&dummy_key);        // 直接用指针 key 访问表,压入对应 valueconst char* pointer_val = lua_tostring(L, -1);std::cout << "my_table[dummy_key] = " << pointer_val << "\n";lua_pop(L, 1);  // 弹出 valuelua_pop(L, 1);  // 弹出 my_table// 关闭 Lua 状态机,释放资源lua_close(L);return 0;
}

你这段内容讲的是 sol3 里对 Lua C API 的封装抽象,主要用 sol::stack 命名空间实现类型安全和简洁的栈操作。

核心理解点:

1. 类型安全的栈操作接口

  • stack::push(v)
    根据 C++ 类型 v 自动选择正确的 Lua API 把值压栈,比如整数、字符串、表、函数都能自动对应正确调用。
  • stack::get<T>(L, index)
    根据模板类型 T 自动用正确的 Lua API 从栈索引 index 取值并转成 C++ 类型。
  • stack::check<T>(L, index)
    用于检查栈上某位置的值是否可以转换成类型 T

2. 组合操作:访问表字段

  • stack::get_field(key, table_index)
    优化实现:
    • 如果 key 是字符串,内部直接调用 lua_getfield(更高效);
    • 如果是全局表,还可能直接调用 lua_getglobal
    • 否则退化调用 lua_gettable
  • stack::set_field(key, value, table_index)
    同理,给表设置字段,字符串键会用 lua_setfield,其他情况用 lua_settable

3. 代码示例解释

lua_State* L = ...;
// 从全局表获取键为 "some_key" 的字段(压栈)
sol::stack::get_field<true>(L, "some_key");
// 从栈顶(-1)拿出 int 类型值
int the_value = sol::stack::get<int>(L, -1);
// 弹出刚才压入的字段值
lua_pop(L, 1);
// 创建一个新表,预留0数组部分,2哈希部分
lua_createtable(L, 0, 2);
// 绑定新表的引用(方便管理)
sol::stack_reference ref(L, -1);
// 给表设置字段(键为1,值为"val1")
sol::stack::set_field(L, 1, "val1");
// 给表设置字段(键为2,值为"val2"),用引用的栈索引
sol::stack::set_field(L, 2, "val2", ref.stack_index());
// 弹出引用的表
ref.pop();

总结

  • sol::stack 让你不必再直接写复杂且易错的 Lua C API 栈操作,转而用类型安全的 C++ 模板函数。
  • 这样提高代码可读性、安全性和开发效率。
  • 你可以专注业务逻辑,减少记忆栈操作细节的负担。
    cmake 直接引入sol2
include_directories(sol2-develop/include)

sol2 example

#include <sol/sol.hpp>
#include <cassert>
int main() {sol::state lua;int x = 0;lua.set_function("beep", [&x] { ++x; });lua.script("beep()");assert(x == 1);
}

给的代码片段是基于 sol::stack 命名空间来操作 Lua 栈的低级接口风格,用于封装Lua栈操作。

#include <sol/sol.hpp>
#include <iostream>
int main() {// 创建 Lua 状态机sol::state lua;lua.open_libraries(sol::lib::base);// 获取原生 lua_State*lua_State* L = lua.lua_state();// 在 Lua 全局表放一个键值对 "some_key" = 123,方便下面读取lua["some_key"] = 123;// 从全局环境表获取字段 some_key,压栈sol::stack::get_field<true>(L, "some_key");// 从栈顶取出 int 类型的值int the_value = sol::stack::get<int>(L, -1);std::cout << "the_value = " << the_value << "\n";// 弹出栈顶元素lua_pop(L, 1);// 创建一个新表(数组部分0,哈希部分2)lua_createtable(L, 0, 2);// 绑定栈顶表的引用,方便后续操作sol::stack_reference ref(L, -1);// 给新表设置字段 key=1, value="val1"sol::stack::set_field(L, 1, "val1");// 给新表设置字段 key=2, value="val2",通过引用的栈索引访问表sol::stack::set_field(L, 2, "val2", ref.stack_index());// 弹出引用表(会自动弹出表对象)ref.pop();return 0;
}

说明

  • sol::state 是 sol3 管理 Lua 状态的 RAII 封装,创建并自动关闭 Lua。
  • lua.lua_state() 返回裸指针 lua_State*,方便使用 sol::stack 低级API。
  • sol::stack::get_field<true>(L, "some_key") 访问全局环境表(因为 true),优化调用 lua_getglobal
  • sol::stack_reference 管理 Lua 栈上对象的引用,避免栈平衡问题。
  • sol::stack::set_field 支持给表设置字段,自动选择合适的 API。

虽然 sol::stack 提供了对 Lua 栈的封装,但它还是基于栈索引操作,比较接近底层 C API 的用法,对新手尤其是非 C/C++ 背景的开发者,门槛还是有点高。

核心意思:

  • 操作栈(stack)和索引仍然是必要的细节,这对纯 C 背景的程序员比较自然,但对新手不友好。
  • C++ 有能力抽象更高层次的概念,复用数据结构,让 Lua 对象映射到 C++ 对象更直观。
  • 比如把 Lua 中的表、函数、userdata 这些核心概念,直接封装成 C++ 类型,形成“面向对象”的接口。

举例说明

  • sol::table — 代表 Lua 表,你可以用 operator[] 访问字段,像操作 std::map/JSON 一样自然。
  • sol::function — 代表 Lua 函数,C++ 里直接调用,像调用普通函数一样简单。
  • sol::userdata — 代表 Lua userdata,封装复杂的 C++ 对象,方便绑定。

为什么要更高层?

  • 简化代码:不用频繁关心栈平衡、索引位置,减少错误。
  • 提升可读性和维护性:代码更符合 C++ 习惯,更容易理解和扩展。
  • 降低学习曲线:Lua API 很庞杂,新手不用背那么多函数。
    所以,sol3 在提供了 sol::stack 这样“低层”栈接口的同时,最重要的是提供这些高层面向对象的封装,让用户像用普通 C++ 容器和函数一样操作 Lua 对象。

sol3 对 Lua 对象引用管理的抽象,尤其是“引用计数”机制,确保 Lua 对象在 C++ 中被安全管理,避免内存泄漏或野指针。

重点理解:

Lua 的 C 注册表(registry)

  • Lua 的 registry 是 Lua 内部的一个特殊表,用来存储 C API 需要长期引用的 Lua 对象(表、函数、线程等)。
  • 这些对象通过“引用”来管理,防止被 Lua GC 回收。

抽象层 0(Abstraction Layer 0)

  • sol3 提供了 sol::reference,它封装了对 Lua registry 的引用。
  • 这个引用会跟踪对象引用计数:
    • 复制(copy)时增加计数,保证多个引用共享所有权。
    • 移动(move)时转移所有权,避免重复计数。
    • 销毁(destruct)时减少计数,引用计数归零时释放 Lua 对象引用。
  • 这个设计类似 C++ 中智能指针(如 boost::intrusive_ptr 或未来的 std::retain_ptr),是一种基于引用计数的智能管理

“Rule of 0”

  • C++ 中 Rule of 0 指的是对象的生命周期自动管理,无需手写拷贝构造、赋值和析构。
  • sol3 的 sol::reference 遵循这个原则,通过复制、移动和析构操作自动管理 Lua 对象生命周期,用户不用手动释放引用。

这样做的好处:

  • 安全:避免使用过期的 Lua 对象指针。
  • 方便:不用关注 Lua 对象的释放细节,降低出错概率。
  • 现代 C++ 风格:更自然、更符合 C++ 智能指针管理资源的习惯。

sol3 封装 Lua 对象的更高层抽象设计,分了三层:

Abstraction Layer 1: 复用现有的 sol::reference

  • sol::objectsol::functionsol::table 都是继承或包含了 sol::reference 的功能。
  • 它们只在 sol::reference 的基础上添加少量特定的成员函数:
    • sol::object 增加 .as<T>()(转换成 C++ 类型)和 .is<T>()(检查类型)。
    • sol::function 增加 .call() 和重载 operator(),可以像普通函数一样调用。
    • sol::table 增加 .set().get()operator[],可以像访问容器一样访问表字段。

Abstraction Layer 2: 代理(Proxy)

  • 目的是让 Lua 表元素访问和赋值写法看起来很自然,比如:
    my_table["foo"] = "bar";
    std::string bar = my_table["foo"];
    int x = my_table["bark"]["bjork"];
    
  • 实现手段是定义一个模板 proxy 结构体,里面有一个模板隐式转换操作符
    template<typename T>
    operator T() { /* 取值操作 */ }
    
  • 同时,proxy 还实现赋值操作符和 operator[],支持“链式”访问。

Layer 2 的问题

  • 某些 C++ 习惯用法会暴露代理模式的缺陷,比如:
    int a, b, c;
    std::tie(a, b, c) = f();
    
  • 这个需要函数返回 std::tuple<int&, int&, int&>,或者至少是可解包的 tuple 类型,但 proxy 的隐式转换无法控制返回类型,只能返回单个值。
  • 代理不能直接返回 std::tuple<int, int, int>,导致和标准库接口的兼容性问题。

可能的解决方案

  • C++ 标准化委员会正在考虑的一个提案(p1193),是为了让隐式转换可以支持显式返回类型(“Explicit Return Types for Implicit Conversion”),以解决这类问题。
  • 未来可能通过 proxy 的模板隐式转换操作符返回更灵活的类型,兼容解包、tuple 等。

总结

  • sol3 通过多层抽象,把 Lua 对象映射为 C++ 类型,接口越做越自然友好。
  • 代理模式让访问和赋值写法直观,但有 C++ 类型系统的限制。
  • 标准提案正在寻求解决这些限制,让代理更强大。

加上详细中文注释,并说明每步的含义,方便理解:

#include <sol/sol.hpp>   // 引入 sol3 头文件
#include <iostream>
int main() {sol::state lua;      // 创建一个新的 Lua 状态机(Lua虚拟机)lua.open_libraries(sol::lib::base);  // 打开标准 Lua 库(比如 print、math 等基础库)// 创建一个新的 Lua 表,赋值给 C++ 的 sol::table 类型变量sol::table my_table = lua.create_table();// 通过代理(proxy)语法向 Lua 表赋值my_table["foo"] = "bar";   // Lua 中 my_table.foo = "bar"my_table["num"] = 42;      // Lua 中 my_table.num = 42// 通过代理语法从 Lua 表读取值,隐式转换成对应 C++ 类型std::string foo_value = my_table["foo"];  // 读取字符串 "bar"int num_value = my_table["num"];           // 读取数字 42// 输出读取到的值std::cout << "my_table['foo'] = " << foo_value << "\n";std::cout << "my_table['num'] = " << num_value << "\n";// 链式访问,自动生成代理,实现类似 my_table.nested.inner 的访问my_table["nested"] = lua.create_table();  // 新建嵌套表 my_table.nested = {}my_table["nested"]["inner"] = 123;         // 嵌套赋值 my_table.nested.inner = 123// 读取嵌套表中的值int nested_inner = my_table["nested"]["inner"];std::cout << "my_table['nested']['inner'] = " << nested_inner << "\n";// 通过 Lua 脚本定义一个简单的加法函数lua.script("function add(a, b) return a + b end");// 通过 sol::function 把 Lua 函数包装成可调用的 C++ 对象sol::function add = lua["add"];// 调用 Lua 中的 add 函数int result = add(3, 4);std::cout << "add(3,4) = " << result << "\n";// 把 my_table 放回 Lua 全局变量空间lua["my_table"] = my_table;// 定义一个 Lua 函数,接收表作为参数并访问表字段lua.script(R"(function call_table_num(t)return t["num"] * 2end)");// 获取 Lua 函数 call_table_num 的引用sol::function call_table_num = lua["call_table_num"];// 传入 C++ 中的 my_table 调用 Lua 函数int doubled = call_table_num(my_table);std::cout << "call_table_num(my_table) = " << doubled << "\n";return 0;
}

代码要点总结:

  • sol::state:管理 Lua 虚拟机,所有 Lua 交互都在这个状态机中进行。
  • sol::table:封装 Lua 表,通过 operator[] 和代理对象支持自然访问。
  • 代理(proxy)机制:让 my_table["foo"] 既可以赋值也能隐式转换读取。
  • sol::function:封装 Lua 函数,可以像普通 C++ 函数一样调用。
  • Lua 脚本与 C++ 混合:可以在 C++ 里写 Lua 脚本,定义函数,之后在 C++ 中调用。

什么是 Usertypes?

  • Usertypes 是 C++ 类和 Lua 之间的“胶水”
    它让你可以把 C++ 的类、对象、成员函数、属性直接暴露给 Lua,Lua 代码就像操作普通的 Lua 表和函数一样访问你的 C++ 对象。
  • 写法干净利落、效率高
    不用写复杂的绑定代码,sol3 自动帮你搞定调用约定、生命周期管理、类型转换等等。
  • 支持大规模代码库渐进集成
    sol3 支持用 sol::state_view 把 Lua 状态机包装成轻量引用,这样你可以在已有项目中慢慢引入 Lua,不用一次性改动所有代码。
  • 功能丰富
    你可以暴露类的构造函数、重载运算符、成员变量,甚至支持继承和多态。
  • 实时感受绑定的神奇
    在 demo 里直接用 Lua 操作 C++ 对象,看到 C++ 类方法、属性在 Lua 里被调用和修改,感受无缝交互。

举个简短的 Usertype 示例:

#include <sol/sol.hpp>  // 引入 sol3 头文件,提供 Lua 与 C++ 交互功能
#include <cmath>        // 引入 cmath 用于 std::sqrt 函数
// 定义一个简单的二维向量类 Vec2
struct Vec2 {float x, y;  // 成员变量 x 和 y// 构造函数,初始化 x 和 yVec2(float x_, float y_) : x(x_), y(y_) {}// 计算向量长度的成员函数float length() const { return std::sqrt(x * x + y * y); }
};
int main() {sol::state lua;          // 创建一个 Lua 状态机(Lua 虚拟机)lua.open_libraries();    // 打开 Lua 标准库,支持基础函数如 print、math 等// 注册 C++ 类 Vec2 到 Lua 中:// "Vec2" 是 Lua 中类名// sol::constructors<Vec2(float, float)>() 注册带两个 float 参数的构造函数// "x", &Vec2::x 绑定成员变量 x,允许 Lua 读写// "y", &Vec2::y 绑定成员变量 y// "length", &Vec2::length 绑定成员函数 length,Lua 中调用时写作 v:length()lua.new_usertype<Vec2>("Vec2", sol::constructors<Vec2(float, float)>(), "x", &Vec2::x, "y", &Vec2::y, "length", &Vec2::length);// 运行一段 Lua 脚本测试绑定的类和函数lua.script(R"(v = Vec2.new(3, 4)          -- 创建 Vec2 实例,调用构造函数print(v.x, v.y, v:length()) -- 访问成员变量 x, y,调用成员函数 length,输出:3 4 5)");return 0; // 程序结束
}

这种绑定方式就是 Usertypes 的魅力所在。你可以让 Lua 脚本很自然地访问你的 C++ 类,像操作 Lua 表一样,既方便又高效。

“Customization Points”和“Defaults”的演进,实际上是在讲 sol3 如何设计灵活且可扩展的类型绑定机制。总结一下核心点:

1. 过去的方案:模板结构体特化(Struct Specializations)

  • sol3 早期通过模板结构体(getter/checker/pusher)来定制如何把 C++ 类型和 Lua 交互。
  • 用户可以通过对这些模板结构体做特化,来自定义自定义类型的绑定行为。
  • 这种方式可以用,但有明显缺点:
    • 用户写起来比较复杂,必须要理解模板元编程和 SFINAE(Substitution Failure Is Not An Error)。
    • 如果特化之间不互斥,很容易产生冲突和编译错误。
    • 扩展难度大,限制了用户定制的灵活性。

2. 现代设计:函数作为自定义点(Customization Points)

  • sol3 改用**函数重载 + ADL(Argument Dependent Lookup)优先级标签(priority tags)**来处理类型转换定制。
  • 用户只需定义普通函数(例如 sol_unqualified_get),函数重载的方式自然解决了优先级和可扩展性问题。
  • 设计思想:
    • 以函数的形式提供“钩子”,允许用户覆盖默认行为。
    • 通过“优先级标签”控制函数调用顺序(高优先级先被调用)。
    • 保留旧的模板结构体特化作为“fallback”(回退)和默认实现,确保兼容和完整性。

3. 优点

  • 用户只写普通函数,不必理解模板特化或复杂的 SFINAE
  • 函数在编译和链接时处理,编译速度快,调试方便
  • 更加模块化,可维护性强,用户扩展更简单。
  • 降低了用户使用 sol3 进行高级定制的门槛。

总结

这就是 sol3 设计上的一大进步:

结构体特化的复杂模板元编程,进化到更自然的基于函数的自定义点机制,既保留了灵活性,又降低了使用复杂度。

http://www.dtcms.com/a/267147.html

相关文章:

  • 高频交易服务器篇
  • 鸿蒙学习笔记
  • 【单片机毕业设计17-基于stm32c8t6的智能倒车监测系统】
  • android studio 配置硬件加速 haxm
  • Java 大视界 -- Java 大数据在智能安防周界防范系统中的智能感知与自适应防御(333)
  • elementui表格增加搜索功能
  • ✨ OpenAudio S1:影视级文本转语音与语音克隆Mac整合包
  • 2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--中篇
  • Ubuntu:Mysql服务器
  • 用户中心Vue3网页开发(1.0版)
  • 类图+案例+代码详解:软件设计模式----适配器模式
  • HarmonyOS学习3---ArkUI
  • Java零基础笔记01(JKD及开发工具IDEA安装配置)
  • 【Linux网络篇】:网络中的其他重要协议或技术——DNS,ICMP协议,NAT技术等
  • STM32第十四天串口
  • uniapp启动图被拉伸问题
  • 国产 OFD 标准公文软件数科 OFD 阅读器:OFD/PDF 双格式支持,公务办公必备
  • React Hooks 内部实现原理与函数组件更新机制
  • 【LeetCode 热题 100】73. 矩阵置零——(解法二)空间复杂度 O(1)
  • stm32的三种开发方式
  • Zigbee/Thread
  • 车载以太网-防火墙
  • 【深度学习】强化学习(Reinforcement Learning, RL)主流架构解析
  • 2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--下篇
  • React Native 开发环境搭建--mac--android--奔溃的一天
  • App爬虫实战篇-以华为真机手机爬取集换社的app为例
  • Pytest 测试发现机制详解:自动识别测试函数与模块
  • 在 Ubuntu 下配置 oh-my-posh —— 普通用户 + root 各自使用独立主题(共享可执行)
  • Redis Cluster 与 Sentinel 笔记
  • 文本方式和二进制方式打开文件的不同