基于 Linux+CMake 从零集成 Lua 脚本引擎 (附 Sol2 避坑指南)

为什么我们的设备需要脚本引擎?

在现代测试测量仪器(如源表 SourceMeter、示波器、电子负载等)的开发中,我们通常会提供精美的 GUI 界面供用户手动操作,并提供 SCPI 指令供上位机远程控制。

但这往往不够。在工业自动化流水线或复杂的科研测试场景中,客户经常需要设备脱机自动执行一连串的复杂动作 (例如:设置电压 -> 延时 -> 测量 -> 条件判断 -> 循环扫描)。如果全靠上位机发指令,通信延迟会极大影响测试吞吐量。因此,提供一种设备端的二次开发脚本语言(如吉时利的 TSP 技术)几乎是高端仪器的标配。

经过多方技术选型,在 C++ 工程中引入了 Lua 最合适。

为什么是 Lua + Sol2?

  • Lua 的极致轻量:官方源码仅几十个文件,几百 KB 大小,运行极快,是嵌入式设备的天选之子。
  • Sol2 的优雅绑定 :原生的 Lua C-API 需要开发者疯狂操作"虚拟栈"(lua_pushlua_pop),极其痛苦且容易内存泄漏。而 Sol2 是一个仅头文件(Header-only)的 C++17 库,它利用现代 C++ 的模板元编程,让你能用一行代码将 C++ 的类、函数、变量直接暴露给 Lua。

本文将手把手带你在一台基于 Linux + CMake + C++17 构建的设备中,从零搭建这套 Lua 脚本引擎。


🛠️ 第一步:环境准备与源码目录构建

与其他庞大的第三方库不同,引入 Lua 和 Sol2 最优雅的方式是源码级集成,直接让它们跟着你的主工程一起编译。

1. 获取 Lua 源码 (避坑警告 ⚠️)

  1. 前往 Lua 官方 FTP 下载稳定版源码。强烈建议下载 Lua 5.4.2(经过实测,该版本与当前 Sol2 v3.3 的兼容性最为完美,能避开新版 Lua 底层 API 变更导致的编译报错)。
  2. 解压后,将其 src 目录下的所有 .c.h 文件拷贝到你工程的 thirdparty/lua/src/ 目录下。
  3. 💣 高能避坑必须删除 lua.cluac.c 这两个文件!
  • 原因 :这两个文件是 Lua 官方提供的命令行解释器(REPL)和编译器入口,里面都包含了独立的 main() 函数。如果不删,后续 make 编译时必然会报 multiple definition of 'main' 的致命错误!

2. 获取 Sol2 库

  1. 前往 GitHub 的 ThePhD/sol2 仓库,在 Releases 中下载最新的 v3.x 版本(必须是 v3,才支持 C++17)。
  2. 下载其单文件版本(Single-header),你会得到一个 sol.hpp
  3. 将其放入工程的 thirdparty/sol2/sol/ 目录下。

最终,你的工程目录树应该长这样:

text 复制代码
MyProject/
├── CMakeLists.txt         # 你的工程主构建脚本
├── main.cpp               # 你的主程序
├── Script/                # 稍后用来存放引擎封装代码的目录
└── thirdparty/            # 第三方依赖
    ├── lua/
    │   └── src/           # 包含几十个 .c 和 .h (已删 lua.c/luac.c)
    └── sol2/
        └── sol/
            └── sol.hpp    # Sol2 核心头文件

⚙️ 第二步:CMakeLists.txt 核心配置

代码放好后,我们需要在 CMake 中把它们"粘合"起来。这里有三个核心关键点:开启 C++17汇集源文件链接系统依赖

在你的 CMakeLists.txt 中添加以下内容:

cmake 复制代码
# 1. 强制开启 C++17 标准 (Sol2 v3 的硬性要求,否则大量模板报错)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 2. 搜集 Lua 源码 (匹配我们刚刚放进去的 .c 文件)
file(GLOB_RECURSE LUA_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/lua/src/*.c")

# 3. 添加头文件搜索路径 (让编译器能找到 <lua.h> 和 <sol/sol.hpp>)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/lua/src)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/thirdparty/sol2)

# 4. 将 Lua 源码一并加入你的执行目标编译列表中
target_sources(${PROJECT_NAME} PRIVATE ${YOUR_APP_SRCS} ${LUA_SRCS})

# 5. Linux 环境下的神秘依赖 (必加)
if(UNIX AND NOT APPLE)
    # Lua 底层可能需要动态加载 C 模块 (dl) 并使用了大量的数学函数 (m)
    target_link_libraries(${PROJECT_NAME} PRIVATE dl m)
endif()

🚀 第三步:封装 C++ 侧的引擎骨架

环境就绪后,我们不要把 Lua 的调用散落在业务代码里。最佳实践是创建一个单例类 ScriptEngine,由它来统筹 Lua 虚拟机的生命周期。

Script/ 目录下新建 ScriptEngine.hppScriptEngine.cpp

ScriptEngine.hpp

💣 高能避坑 :如果你使用的是 Ubuntu 22.04 或 GCC 11 以上的现代编译器,请务必在 #include <sol/sol.hpp> 之前加上 #include <limits>,否则会报 numeric_limits is not a member of std 的错!

cpp 复制代码
/**
 * @file ScriptEngine.hpp
 * @brief Lua 脚本引擎核心封装
 */
#pragma once

#include <limits>      // 修复 GCC11+ 下 sol2 的 numeric_limits 报错
#include <sol/sol.hpp>
#include <string>

namespace APP {

class ScriptEngine {
public:
    // 获取单例
    static ScriptEngine& GetInstance() {
        static ScriptEngine instance;
        return instance;
    }

    // 初始化虚拟机
    void Init();

    // 运行一段代码字符串
    void RunString(const std::string& script);

    // 运行一个本地 .lua 文件
    bool RunFile(const std::string& filepath);

private:
    ScriptEngine() = default;
    ~ScriptEngine() = default;

private:
    sol::state lua_; // Sol2 包装的 Lua 虚拟机状态机
};

} // namespace APP

ScriptEngine.cpp

cpp 复制代码
#include "ScriptEngine.hpp"
#include <iostream>

namespace APP {

void ScriptEngine::Init() {
    // 1. 打开 Lua 的基础标准库 (math, string, os, table 等)
    // 这样脚本里就能愉快地使用 string.format 和 math.sin 啦
    lua_.open_libraries(sol::lib::base, 
                        sol::lib::math, 
                        sol::lib::string, 
                        sol::lib::os, 
                        sol::lib::table);

    // 2. (可选) 验证一下是否联通
    std::cout << "[ScriptEngine] Lua 虚拟机初始化完成!" << std::endl;
}

void ScriptEngine::RunString(const std::string& script) {
    try {
        lua_.script(script);
    } catch (const sol::error& e) {
        std::cerr << "Lua 语法/运行错误: " << e.what() << std::endl;
    }
}

bool ScriptEngine::RunFile(const std::string& filepath) {
    try {
        auto result = lua_.script_file(filepath);
        if (!result.valid()) {
            sol::error err = result;
            std::cerr << "执行文件失败: " << err.what() << std::endl;
            return false;
        }
        return true;
    } catch (const std::exception& e) {
        std::cerr << "发生异常: " << e.what() << std::endl;
        return false;
    }
}

} // namespace APP

✅ 第四步:点亮你的脚本引擎

最后,在你的 main.cpp 中引入并调用它:

cpp 复制代码
#include "Script/ScriptEngine.hpp"
#include <iostream>

int main() {
    std::cout << "系统启动中..." << std::endl;

    // 1. 初始化引擎
    APP::ScriptEngine::GetInstance().Init();

    // 2. 跑一句经典的 Hello World 压压惊
    APP::ScriptEngine::GetInstance().RunString("print('Hello from Lua! 1+1 = ' .. tostring(1+1))");

    // 你的主程序循环...
    while(1) {
        // ...
    }
    return 0;
}

执行 make 编译,如果你能在终端看到打印出的 Hello from Lua! 1+1 = 2,恭喜你!这台 C++ 仪器的"跨次元壁"大脑已经成功点亮了!


🔮 总结与下期预告

在这篇文章中,我们扫清了环境搭建、CMake 链接、版本冲突等一系列外围障碍,成功在 Linux C++ 程序内部养了一只轻盈的 Lua 虚拟机。

但这套代码还远不能投入工业使用!

在一个带有 UI(比如 LVGL)的仪器中,如果你直接调用 RunFile() 跑一个带有长延时(delay)甚至死循环(while true)的脚本,整个仪器的触控和界面会瞬间卡死(UI 线程被阻塞)! 在下一篇《实战篇:拒绝 UI 卡死!C++ 仪器系统中的 Lua 脚本多线程与硬件绑定揭秘》中,我将硬核分享:

  1. 多线程调度:如何把 Lua 关进后台独立线程?
  2. 底层硬件绑定 :如何用 Sol2 把 C++ 的底层控制类(如设置电压、读取电流)暴露给 Lua smu 命名空间?
  3. 安全拦截器:用户写了死循环怎么办?如何利用 Lua Hook 机制实现"强杀"?
相关推荐
我爱学习好爱好爱2 小时前
Elasticsearch 7.17.10 双节点集群部署实战(基于 Rocky Linux 9.6)
大数据·linux·elasticsearch
NEAI_N2 小时前
离网设备的加密解密方案
linux·服务器·网络
左手の明天2 小时前
Linux内核裁剪深入浅出:从原理到实操,打造轻量化嵌入式内核
linux·arm开发·c++
萝卜白菜。2 小时前
annotation扫描引起的StackOverflowError问题
linux·运维·服务器
瀚高PG实验室2 小时前
瀚高安全版 V4.5.10卸载后残留了db_ha的agent进程导致6666端口被占用
linux·数据库·安全·瀚高数据库
HHFQ2 小时前
如何仅使用键盘通过图形界面安装 RHEL 等 Linux 发行版
linux·运维
枕布响丸辣2 小时前
Linux 系统安全及应用实战:账号、引导、弱口令与端口扫描全解析
linux·运维·系统安全
酸菜。2 小时前
lspci -tv使用
linux
zzh0812 小时前
Linux系统安全
linux·运维·系统安全