C 扩展开发指南:从 Lua 到 skynet 的完整链路

C 扩展开发指南:从 Lua 到 skynet 的完整链路

读完这篇你会明白:C 扩展到底是什么、skynet 怎么加载它、怎么从零写一个、C++ 怎么写、有哪些坑。


一、前置概念:这不是 skynet 发明的

很多人以为「skynet C 扩展」是 skynet 特有的东西,其实本质是 Lua 语言自带的能力。

任何嵌入了 Lua 虚拟机的程序(nginx、Redis、魔兽世界、Wireshark),都能用同一套机制加载 .so 扩展。

skynet 做的事情只有两件:

  1. 告诉 Lua 去哪里找 .so (配置 lua_cpath 路径)
  2. 用自己的 skynet.require 包了一层协程管理 (底层最终还是调 Lua 原生 require)
lua 复制代码
-- config.path 里这行就是 skynet 所做的「全部工作」
lua_cpath = root .. "luaclib/?.so;" .. "./chatroom/luaclib/?.so"

-- 然后 Lua 的 require 就能找到你的 pbkdf2.so 了
local pbkdf2 = require "pbkdf2"

二、加载链路:从 require "xxx" 到你的 C 函数被执行

当你写 local pbkdf2 = require "pbkdf2",完整链路如下:

复制代码
Lua 代码: require "pbkdf2"
  │
  ▼
Lua 虚拟机: package.searchpath("pbkdf2", package.cpath)
  │  package.cpath = "xxx/luaclib/?.so"
  │  遍历每个路径,把 ? 替换成 pbkdf2
  │  → 找到 xxx/luaclib/pbkdf2.so ✓
  │
  ▼
package.loadlib("xxx/luaclib/pbkdf2.so", "luaopen_pbkdf2")
  │  ├─ dlopen()   把 .so 加载到进程内存
  │  └─ dlsym()    查找名叫 luaopen_pbkdf2 的 C 函数
  │
  ▼
luaopen_pbkdf2(L) 被调用
  │  你的代码在这里注册函数:
  │  luaL_Reg lib[] = { {"hash", lpbkdf2_hash}, {NULL, NULL} };
  │  luaL_newlib(L, lib);   // 生成模块表 { hash = function }
  │  return 1;
  │
  ▼
Lua 拿到: pbkdf2 = { hash = <C函数指针> }

调用: pbkdf2.hash("password", "salt", 10000)
  │
  ▼
lpbkdf2_hash(L) 执行
  │  从 Lua 栈读参数 → 计算 → 结果压回 Lua 栈
  │  return 1  // 返回 1 个值
  │
  ▼
Lua 拿到返回值: "3f8a9b..."

三、上手:从零写一个 C 扩展(完整示例)

3.1 最小骨架

c 复制代码
// mymath.c --- 一个最简单的 Lua C 扩展

#include <lua.h>         // Lua 核心 API: lua_State, lua_push*, luaL_error...
#include <lauxlib.h>     // 辅助宏: luaL_check*, luaL_newlib, luaL_Reg...

// 暴露给 Lua 的函数: 计算 a + b
static int ladd(lua_State *L) {
    // 从 Lua 栈读取参数(栈底索引 = 1)
    double a = luaL_checknumber(L, 1);   // 第 1 个参数
    double b = luaL_checknumber(L, 2);   // 第 2 个参数

    // 计算结果,压入 Lua 栈作为返回值
    lua_pushnumber(L, a + b);

    // return 1: 告诉 Lua "我返回了 1 个值"
    return 1;
}

// 模块入口: 函数名必须是 luaopen_<模块名>
int luaopen_mymath(lua_State *L) {
    // luaL_Reg: Lua 名字 → C 函数指针 的映射表
    luaL_Reg funcs[] = {
        { "add", ladd },      // Lua 调 mymath.add() → C 执行 ladd()
        { NULL, NULL }        // 必须以 NULL 结尾
    };
    luaL_newlib(L, funcs);    // 创建模块表并压栈
    return 1;                 // 模块表作为唯一返回值
}

编译:

bash 复制代码
gcc -shared -fPIC -o mymath.so mymath.c -I/usr/include/lua5.3

Lua 测试:

lua 复制代码
local mymath = require "mymath"
print(mymath.add(1, 2))   -- 输出: 3.0

3.2 我们的 PBKDF2 扩展(实际项目代码)

源码: chatroom/cservice/pbkdf2.c

c 复制代码
#include <lua.h>
#include <lauxlib.h>
#include <string.h>
#include <openssl/evp.h>

static int lpbkdf2_hash(lua_State *L) {
    // 从 Lua 栈读参数
    size_t pass_len, salt_len;
    const char *password = luaL_checklstring(L, 1, &pass_len);
    const char *salt     = luaL_checklstring(L, 2, &salt_len);
    int rounds           = (int)luaL_checkinteger(L, 3);

    if (rounds <= 0) {
        luaL_error(L, "rounds must be > 0");
    }

    // PBKDF2-HMAC-SHA256: 输出 32 字节密钥
    unsigned char out[32];
    PKCS5_PBKDF2_HMAC(password, (int)pass_len,
                      (const unsigned char *)salt, (int)salt_len,
                      rounds, EVP_sha256(), 32, out);

    // 转 64 字符 hex
    char hex[65];
    for (int i = 0; i < 32; i++) {
        sprintf(hex + i * 2, "%02x", out[i]);
    }
    hex[64] = '\0';

    lua_pushstring(L, hex);
    return 1;
}

int luaopen_pbkdf2(lua_State *L) {
    luaL_Reg lib[] = {
        { "hash", lpbkdf2_hash },
        { NULL, NULL }
    };
    luaL_newlib(L, lib);
    return 1;
}

Lua 侧调用:

lua 复制代码
local pbkdf2 = require "pbkdf2"
local hash = pbkdf2.hash("mypassword", "a1b2c3d4e5f6a7b8", 10000)
-- hash = "3f8a9b2c..."  (64 字符 hex)

四、编写规则速查表

# 规则 说明
1 头文件 #include <lua.h> + <lauxlib.h>,C++ 用 <lua.hpp>
2 入口命名 必须是 luaopen_<模块名>,require "foo" 找 luaopen_foo
3 函数签名 int func(lua_State *L),参数从栈读,返回值压栈
4 读参数 luaL_checklstring(字符串)、luaL_checkinteger(整数)、luaL_checknumber(浮点)
5 报错 只能用 luaL_error(L, "msg") ,不能用 exit() 或 return -1
6 返回值 lua_pushstring / lua_pushinteger / lua_pushnumber 压栈,return N 告知个数
7 注册模块 luaL_Reg 数组做名字映射,luaL_newlib 生成模块表
8 栈平衡 luaopen_xxx 必须 return 1(模块表);普通函数 return 返回值个数
9 编译 输出 .so 到 luaclib/,lua_cpath 指向它
10 名字映射 { "lua函数名", c函数指针 },两者可以完全不同

五、skynet 的两个 "cpath"(容易混淆)

skynet config 里有两个 cpath 相关配置,职责完全不同:

复制代码
┌──────────────────────────────────────────────────────────┐
│  config 里的 cpath = root.."cservice/?.so"               │
│  ─────────────────────────────────────                   │
│  给 skynet 自己的 C Service 用                           │
│  接口: _create / _init / _release / _signal              │
│  例子: snlua.so, gate.so, harbor.so, logger.so           │
│  加载者: skynet_module.c (skynet 自己的代码)              │
│                                                          │
│  config.path 里的 lua_cpath = root.."luaclib/?.so"       │
│  ─────────────────────────────────────                   │
│  给 Lua 标准 C 模块用                                    │
│  接口: luaopen_xxx                                       │
│  例子: md5.so, sproto.so, pbkdf2.so                      │
│  加载者: Lua 虚拟机 package.loadlib                      │
└──────────────────────────────────────────────────────────┘

一句话区分 :cpath 是 skynet 的;lua_cpath 是 Lua 的。


六、C++ 写 Lua 扩展

C++ 和 C 的核心差异就三点:

6.1 extern "C" 阻止名字改编

cpp 复制代码
// C++ 编译器会把 luaopen_pbkdf2 改名成 _Z15luaopen_pbkdf2P9lua_State
// Lua 的 dlsym("luaopen_pbkdf2") 就找不到了
// 必须用 extern "C" 包住入口函数

extern "C" int luaopen_pbkdf2(lua_State *L) {
    luaL_Reg lib[] = {
        { "hash", lpbkdf2_hash },
        { nullptr, nullptr }
    };
    luaL_newlib(L, lib);
    return 1;
}

6.2 异常不能穿透 Lua 栈

cpp 复制代码
static int lpbkdf2_hash(lua_State *L) {
    try {
        // 你的 C++ 代码...
        std::string result = do_something();
        lua_pushstring(L, result.c_str());
        return 1;
    } catch (const std::exception& e) {
        // 必须在这里捕获,转成 Lua 错误
        luaL_error(L, "internal error: %s", e.what());
    }
    return 0;
}

6.3 longjmp 跳过析构(最大坑)

luaL_error 内部用的是 longjmp,会直接跳过 C++ 栈上对象的析构函数,导致内存泄漏。

cpp 复制代码
// ❌ 危险写法
static int bad_func(lua_State *L) {
    std::string s = "hello";          // RAII 对象
    std::vector<int> v = {1,2,3};     // 堆内存

    if (something_wrong)
        luaL_error(L, "oops");         // longjmp!! s 和 v 不会被析构!!
}

// ✅ 安全写法: 用作用域 + try/catch 隔离
static int good_func(lua_State *L) {
    try {
        std::string s = "hello";       // RAII 对象在 try 块内
        std::vector<int> v = {1,2,3};

        if (something_wrong)
            throw std::runtime_error("oops");   // 抛异常(不跨 longjmp)

        return do_work(L, s, v);       // 正常返回,s/v 正确析构

    } catch (const std::exception& e) {
        // 到这里所有 RAII 对象都已析构,安全调 luaL_error
        luaL_error(L, "%s", e.what());
    }
    return 0;
}

七、常见问题

Q1: C 扩展和 skynet C Service 是一回事吗?

不是。 C 扩展是 Lua 标准模块(luaopen_xxx 入口),被 require 加载后在 Lua 服务里调用。skynet C Service(如 snlua.so)是 skynet 自己的服务框架,需要 _create/_init/_release/_signal 四个接口,有独立的消息队列和调度。

Q2: 能在 C 扩展里调 skynet API 吗?

不能。 C 扩展只是被 require 加载的一个函数库,运行在调用它的 Lua 服务的线程里,没有自己的 skynet_context。如果要访问 skynet 的 send/call/timer 等功能,需要通过 Lua 层传过来。

Q3: 性能提升有多大?

以我们的 PBKDF2 为例:旧的纯 Lua 方案 10000 次 MD5 循环约需 5ms(每次都要 Lua↔C FFI 边界跨越),C 扩展一包到底只需要 ~0.1ms。性能差距 ≈ 50 倍,原因就是省掉了维度跨越开销。

Q4: Rust / Go 能写 Lua 扩展吗?

Rust 可以(#[no_mangle] pub extern "C" fn luaopen_xxx),社区有成熟的 mlua/rlua 库。Go 理论上也能(cgo + //export),但不推荐------Go 的 goroutine 调度器和 Lua 的 longjmp 不兼容,一不小心就 segfault。


八、文件目录约定

复制代码
chatroom/
├── cservice/              ← C 扩展源码放这里
│   └── pbkdf2.c
├── luaclib/               ← 编译出的 .so 放这里(gitignore)
│   └── pbkdf2.so
├── Makefile               ← 编译命令
└── etc/
    └── config.path        ← lua_cpath 指向 luaclib/

九、相关源码索引

想看什么 文件
编译脚本 Makefile
Lua 侧调用入口 service/auth.lua#L48
skynet loader 设置 cpath skynet/lualib/loader.lua
skynet 自己的 C Service 加载 skynet/skynet-src/skynet_module.c
skynet 自带的 crypt 扩展示例 skynet/lualib-src/lua-crypt.c
相关推荐
Navigator_Z11 小时前
LeetCode //C - 1289. Minimum Falling Path Sum II
c语言·算法·leetcode
himobrinehacken12 小时前
取模运算的汇编奥秘揭秘
c语言·安全
青岚_Nox14 小时前
C语言实战:带你手写一个带"计时功能"的猜数字小游戏
c语言
iotxiaohu16 小时前
用 cflow 查看 C 函数调用关系
c语言·开发语言·cflow
iCxhust19 小时前
51单片机电力载波应用开发(九)51产品化
c语言·数据库·嵌入式硬件·mysql·51单片机·软件工程·c51
Logic10119 小时前
C语言/数据结构字符串题解:统计“chhc“子串权值总和——一次遍历模式匹配
c语言·数据结构·算法·字符串·时间复杂度·空间复杂度·ascii
m0_5474866621 小时前
《数据结构 (C语言)》期末试卷及答案(10套)2026版
c语言·开发语言·数据结构
八月的雨季 最後的冰吻21 小时前
FFmepg-- 42-ffplay.c 线程安全机制深度分析
c语言·安全·ffmpeg
逐光者93321 小时前
滤波器的 R 与 C 如何选择:从截止频率到工程实践
c语言·开发语言·r语言