[mpv脚本系统] (三) C 函数如何注册成 Lua 模块

你有没有好奇过:mpv 启动时,Lua 脚本里写 local utils = require("mp.utils") 就能拿到 readdirsplit_path 这些 C 函数------这些函数是怎么从 C 侧注册到 Lua 侧的?require 又是怎么知道去哪个表里找的?

答案比你想的简单:Lua 的模块本质上就是一个普通 table,而 mpv 在 C 侧用 push_module_table 模拟了 require 的缓存机制------先查缓存、没有再创建、最后往里面灌函数。


文章目录

    • [1. 先理解 Lua 的模块到底是什么](#1. 先理解 Lua 的模块到底是什么)
    • [2. mpv 的模块全景](#2. mpv 的模块全景)
    • [3. 源码剖析:push_module_table](#3. 源码剖析:push_module_table)
    • [4. 源码剖析:register_package_fns](#4. 源码剖析:register_package_fns)
    • [5. 模块注册的完整初始化流程](#5. 模块注册的完整初始化流程)
    • [6. 内置 Lua 脚本的加载方式](#6. 内置 Lua 脚本的加载方式)
    • [7. C 模块 vs Lua 内置脚本:两种注册方式对比](#7. C 模块 vs Lua 内置脚本:两种注册方式对比)
    • [8. 自己动手:写一个最简 C 模块](#8. 自己动手:写一个最简 C 模块)
      • [9.1 定义函数表](#9.1 定义函数表)
      • [9.2 在 add_functions 中注册](#9.2 在 add_functions 中注册)
      • [9.3 Lua 侧使用](#9.3 Lua 侧使用)
    • [9. 结论](#9. 结论)

1. 先理解 Lua 的模块到底是什么

Lua 没有 class、没有 namespace、没有 import 关键字。模块是一个约定 :一个 .lua 文件返回一个 table,require 负责加载它、执行它、把返回值缓存起来。

lua 复制代码
-- my_module.lua
local M = {}
function M.hello() print("hello") end
return M
lua 复制代码
-- main.lua
local mod = require("my_module")  -- 第一次:加载 + 执行 + 缓存
mod.hello()
local mod2 = require("my_module") -- 第二次:直接返回缓存,不重复执行
print(mod == mod2)  -- true,同一个 table

模块 = 一个被 package.loaded 缓存的 table。这个认知是理解 mpv C 侧模块机制的全部前提。


2. mpv 的模块全景

mpv 启动时会在 C 侧注册两个核心模块,并把内置的 Lua 脚本预加载到 package.preload 中:

复制代码
mp  → 核心 API(log, command, get_property, wait_event...)

mp.utils  → 工具函数(readdir, file_info, split_path, join_path...)

内置脚本(通过 package.preload 注册):
  mp.defaults, mp.assdraw, mp.fzy, mp.input, mp.options,
  @osc.lua, @ytdl_hook.lua, @stats.lua, @console.lua...

#mermaid-svg-YbhAeOCFXiu6Pu8r{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YbhAeOCFXiu6Pu8r .error-icon{fill:#552222;}#mermaid-svg-YbhAeOCFXiu6Pu8r .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YbhAeOCFXiu6Pu8r .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .marker.cross{stroke:#333333;}#mermaid-svg-YbhAeOCFXiu6Pu8r svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YbhAeOCFXiu6Pu8r p{margin:0;}#mermaid-svg-YbhAeOCFXiu6Pu8r .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .cluster-label text{fill:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .cluster-label span{color:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .cluster-label span p{background-color:transparent;}#mermaid-svg-YbhAeOCFXiu6Pu8r .label text,#mermaid-svg-YbhAeOCFXiu6Pu8r span{fill:#333;color:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .node rect,#mermaid-svg-YbhAeOCFXiu6Pu8r .node circle,#mermaid-svg-YbhAeOCFXiu6Pu8r .node ellipse,#mermaid-svg-YbhAeOCFXiu6Pu8r .node polygon,#mermaid-svg-YbhAeOCFXiu6Pu8r .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .rough-node .label text,#mermaid-svg-YbhAeOCFXiu6Pu8r .node .label text,#mermaid-svg-YbhAeOCFXiu6Pu8r .image-shape .label,#mermaid-svg-YbhAeOCFXiu6Pu8r .icon-shape .label{text-anchor:middle;}#mermaid-svg-YbhAeOCFXiu6Pu8r .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .rough-node .label,#mermaid-svg-YbhAeOCFXiu6Pu8r .node .label,#mermaid-svg-YbhAeOCFXiu6Pu8r .image-shape .label,#mermaid-svg-YbhAeOCFXiu6Pu8r .icon-shape .label{text-align:center;}#mermaid-svg-YbhAeOCFXiu6Pu8r .node.clickable{cursor:pointer;}#mermaid-svg-YbhAeOCFXiu6Pu8r .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .arrowheadPath{fill:#333333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YbhAeOCFXiu6Pu8r .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YbhAeOCFXiu6Pu8r .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YbhAeOCFXiu6Pu8r .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YbhAeOCFXiu6Pu8r .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .cluster text{fill:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r .cluster span{color:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YbhAeOCFXiu6Pu8r .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YbhAeOCFXiu6Pu8r rect.text{fill:none;stroke-width:0;}#mermaid-svg-YbhAeOCFXiu6Pu8r .icon-shape,#mermaid-svg-YbhAeOCFXiu6Pu8r .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YbhAeOCFXiu6Pu8r .icon-shape p,#mermaid-svg-YbhAeOCFXiu6Pu8r .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YbhAeOCFXiu6Pu8r .icon-shape .label rect,#mermaid-svg-YbhAeOCFXiu6Pu8r .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YbhAeOCFXiu6Pu8r .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YbhAeOCFXiu6Pu8r .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YbhAeOCFXiu6Pu8r :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Lua侧可见
C侧注册
内置脚本
package.preload
mp.defaults
@osc.lua
@stats.lua
add_functions()
mp 模块表

main_fns 数组
mp.utils 模块表

utils_fns 数组
全局变量 _G.mp

无需 require
package.loaded'mp.utils'

需 require('mp.utils')

两个关键差异值得注意:

  • mp 模块 被直接设为全局变量 _G.mp,脚本里写 mp.get_property(...) 不需要 require。这是 mpv 为了方便脚本编写做的特殊处理。
  • mp.utils 模块 走标准的 require("mp.utils") 路径,符合 Lua 常规习惯。

3. 源码剖析:push_module_table

push_module_table 是 C 侧模块注册的基石,它的逻辑只有 12 行:

c 复制代码
// Push the table of a module. If it doesn't exist, it's created.
// The Lua script can call "require(module)" to "load" it.
static void push_module_table(lua_State *L, const char *module)
{
    // 第1步:拿到 package.loaded 表(Lua require 的缓存表)
    lua_getglobal(L, "package");       // [ (-1)package表 ]
    lua_getfield(L, -1, "loaded");     // [ (-2)package表, (-1)loaded表 ]
    lua_remove(L, -2);                 // [ (-1)loaded表 ]

    // 第2步:查缓存 ------ loaded["mp.utils"] 存在吗?
    lua_getfield(L, -1, module);       // [ (-2)loaded表, (-1)module表(或nil) ]

    if (lua_isnil(L, -1)) {
        // 缓存未命中 → 创建新 table 并注册到 loaded
        lua_pop(L, 1);                 // [ (-1)loaded表 ]
        lua_newtable(L);               // [ (-1)loaded表, (-2)新module表 ]
        lua_pushvalue(L, -1);          // [ (-3)loaded表, (-2)新module表, (-1)新module表 ]
        lua_setfield(L, -3, module);   // loaded[module] = 新module表
    }
    // 第3步:清理,栈顶只留 module 表
    lua_remove(L, -2);                 // [ (-1)module表 ]
}

用一句话说就是:package.loaded[module],有就复用,没有就新建一个空 table 塞进去,最后把 module 表留在栈顶

为什么必须在 package.loaded 里注册? 因为 Lua 的 require 就是在这里查缓存的。如果 C 侧不注册,Lua 侧 require("mp.utils") 就会去文件系统找 mp/utils.lua,找不到就报错。


4. 源码剖析:register_package_fns

拿到 module 表之后,register_package_fns 负责往里面灌函数:

c 复制代码
static void register_package_fns(lua_State *L, char *module,
                                 const struct fn_entry *e)
{
    push_module_table(L, module);        // 栈顶现在是 module 表
    for (int n = 0; e[n].name; n++) {
        if (e[n].af) {
            // ★ af 非空 → 走 autofree 包装(三层闭包,自动管理资源)
            af_pushcclosure(L, e[n].af, 0);
        } else {
            // 普通路径 → 直接注册为 lua_CFunction
            lua_pushcclosure(L, e[n].fn, 0);
        }
        lua_setfield(L, -2, e[n].name);  // module.name = fn
    }
    lua_pop(L, 1);  // 弹出 module 表,栈恢复干净
}

这里的关键是 fn_entry 结构体:

c 复制代码
#define FN_ENTRY(name) {#name, script_ ## name, 0}
#define AF_ENTRY(name) {#name, 0, script_ ## name}

struct fn_entry {
    const char *name;                    // Lua 侧的函数名(如 "readdir")
    int (*fn)(lua_State *L);            // 普通 lua_CFunction(af 为 0 时使用)
    int (*af)(lua_State *L, void *);    // 带 autofree 的函数(fn 为 0 时使用)
};

FN_ENTRY(readdir) 展开后是 {"readdir", script_readdir, 0}------af 字段为 0,表示这是普通函数,不需要自动资源管理。而 AF_ENTRY(readdir) 展开后 fn 字段为 0,af 指向带 void *ctx 参数的版本,走 autofree 包装。

utils_fns 数组为例:

c 复制代码
static const struct fn_entry utils_fns[] = {
    AF_ENTRY(readdir),      // 需要打开 DIR*,走 autofree
    FN_ENTRY(file_info),    // 只读 stat,不需要临时资源
    FN_ENTRY(split_path),   // 纯字符串操作
    AF_ENTRY(join_path),    // 需要 talloc 分配拼接路径
    AF_ENTRY(parse_json),   // 需要临时 mpv_node
    AF_ENTRY(format_json),  // 需要临时 mpv_node
    FN_ENTRY(get_env_list),
    FN_ENTRY(terminal_display_width),
    {0}                     // 哨兵,表示数组结束
};

5. 模块注册的完整初始化流程

run_lua 是每个 Lua 脚本启动时的入口函数,模块注册发生在其中:

c 复制代码
static int run_lua(lua_State *L)
{
    luaL_openlibs(L);           // 初始化 Lua 标准库

    add_functions(ctx);         // ★ 注册 mp 和 mp.utils 模块

    // 特殊处理:把 mp 模块直接设为全局变量
    push_module_table(L, "mp");  //[-1: mp 模块表] 
    lua_pushvalue(L, -1);    // [-2: mp 模块表, -1: mp 模块表]
    lua_setglobal(L, "mp");     // _G.mp = mp 模块表
    // ... 设置 script_name、UNKNOWN_TYPE/MAP/ARRAY 元表 ...

    // 为内置 Lua 脚本注册 preloader
    lua_getglobal(L, "package");  // [(-1)package表]
    lua_getfield(L, -1, "preload");  // [(-2)package表, (-1)preload表]
    for (int n = 0; builtin_lua_scripts[n][0]; n++) {
        lua_pushcfunction(L, load_builtin); // [(-3)package, (-2)preload表, (-1)load_builtin]
        lua_setfield(L, -2, builtin_lua_scripts[n][0]);
        // package.preload["mp.defaults"] = load_builtin
        // package.preload["@osc.lua"] = load_builtin
        // ...

        // 这样 Lua 侧 require("mp.defaults") 时就会调用 load_builtin 来加载内置脚本
    }
    // ...
}

其中 add_functions 就是一行一行地调 register_package_fns

c 复制代码
static void add_functions(struct script_ctx *ctx)
{
    lua_State *L = ctx->state;
    register_package_fns(L, "mp",       main_fns);
    register_package_fns(L, "mp.utils", utils_fns);
}

整个初始化流程用 Mermaid 表示
Lua 虚拟机 register_package_fns push_module_table run_lua (C) Lua 虚拟机 register_package_fns push_module_table run_lua (C) #mermaid-svg-GGipEDl45jvZbZZE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GGipEDl45jvZbZZE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GGipEDl45jvZbZZE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GGipEDl45jvZbZZE .error-icon{fill:#552222;}#mermaid-svg-GGipEDl45jvZbZZE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GGipEDl45jvZbZZE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GGipEDl45jvZbZZE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GGipEDl45jvZbZZE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GGipEDl45jvZbZZE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GGipEDl45jvZbZZE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GGipEDl45jvZbZZE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GGipEDl45jvZbZZE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GGipEDl45jvZbZZE .marker.cross{stroke:#333333;}#mermaid-svg-GGipEDl45jvZbZZE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GGipEDl45jvZbZZE p{margin:0;}#mermaid-svg-GGipEDl45jvZbZZE .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GGipEDl45jvZbZZE text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-GGipEDl45jvZbZZE .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GGipEDl45jvZbZZE .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-GGipEDl45jvZbZZE .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-GGipEDl45jvZbZZE .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-GGipEDl45jvZbZZE #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-GGipEDl45jvZbZZE .sequenceNumber{fill:white;}#mermaid-svg-GGipEDl45jvZbZZE #sequencenumber{fill:#333;}#mermaid-svg-GGipEDl45jvZbZZE #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-GGipEDl45jvZbZZE .messageText{fill:#333;stroke:none;}#mermaid-svg-GGipEDl45jvZbZZE .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GGipEDl45jvZbZZE .labelText,#mermaid-svg-GGipEDl45jvZbZZE .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-GGipEDl45jvZbZZE .loopText,#mermaid-svg-GGipEDl45jvZbZZE .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-GGipEDl45jvZbZZE .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GGipEDl45jvZbZZE .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GGipEDl45jvZbZZE .noteText,#mermaid-svg-GGipEDl45jvZbZZE .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-GGipEDl45jvZbZZE .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GGipEDl45jvZbZZE .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GGipEDl45jvZbZZE .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GGipEDl45jvZbZZE .actorPopupMenu{position:absolute;}#mermaid-svg-GGipEDl45jvZbZZE .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-GGipEDl45jvZbZZE .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GGipEDl45jvZbZZE .actor-man circle,#mermaid-svg-GGipEDl45jvZbZZE line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-GGipEDl45jvZbZZE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} luaL_openlibs() add_functions() push_module_table("mp") package.loaded"mp" = {} (新建) 栈顶: mp 表 mp.log = script_log mp.command = script_command ... (遍历 main_fns) push_module_table("mp.utils") package.loaded"mp.utils" = {} (新建) 栈顶: mp.utils 表 mp.utils.readdir = script_readdir mp.utils.file_info = script_file_info ... (遍历 utils_fns) _G.mp = mp 模块表 (直接设为全局) package.preload"mp.defaults" = load_builtin


6. 内置 Lua 脚本的加载方式

mpv 把一些核心 Lua 脚本编译成了 C 字符串常量(通过 .lua.inc 文件),在启动时注册到 package.preload 中:

c 复制代码
static const char * const builtin_lua_scripts[][2] = {
    {"mp.defaults",   // module 名称
#   include "player/lua/defaults.lua.inc"  // 编译期嵌入的 Lua 源码字符串
    },
    {"@osc.lua",
#   include "player/lua/osc.lua.inc"
    },
    // ...
    {0}  // 哨兵
};

#include "xxx.lua.inc" 是构建系统生成的------它把 .lua 文件内容转成 C 字符串字面量,编译进二进制。这样 mpv 不需要在运行时去找 .lua 文件。

当 Lua 脚本执行 require("mp.defaults") 时:

复制代码
require("mp.defaults")
  → 查 package.loaded["mp.defaults"] → nil(未加载)
  → 查 package.preload["mp.defaults"] → load_builtin(找到了!)
  → 调用 load_builtin(L)
    → 从 builtin_lua_scripts 数组中匹配 name
    → luaL_loadbuffer(L, script, strlen(script), ...)  // 加载嵌入的源码
    → lua_call(L, 0, 1)  // 执行,拿到返回值
    → package.loaded["mp.defaults"] = 返回值  // 缓存
c 复制代码
static int load_builtin(lua_State *L)
{
    const char *name = luaL_checkstring(L, 1);  // require("mp.defaults") 传入的模块名
    for (int n = 0; builtin_lua_scripts[n][0]; n++) {
        if (strcmp(name, builtin_lua_scripts[n][0]) == 0) {
            const char *script = builtin_lua_scripts[n][1];
            // 把嵌入的 C 字符串作为 Lua 代码加载并执行
            if (luaL_loadbuffer(L, script, strlen(script), dispname))
                lua_error(L);
            lua_call(L, 0, 1);   // 执行脚本,返回值留在栈顶
            return 1;            // require 会把栈顶值缓存到 package.loaded
        }
    }
    luaL_error(L, "builtin module '%s' not found\n", name);
}

这是 package.preload 的标准用法:preload 里存的不是模块本身,而是一个工厂函数require 先查 loaded,没命中就查 preload,调用工厂函数拿到模块,再塞进 loaded 缓存。


7. C 模块 vs Lua 内置脚本:两种注册方式对比

维度 C 模块(mp / mp.utils) Lua 内置脚本
注册方式 push_module_table + register_package_fns package.preload[name] = load_builtin
函数来源 C 函数指针(lua_CFunction Lua 源码字符串
是否全局可用 mp 是(_G.mp),mp.utils 否(需 require
加载时机 run_lua 中主动调用 add_functions 首次 require 时懒加载
典型用途 高性能 / 底层 API(属性读写) 可选的辅助功能(OSC、ytdl_hook、stats)

下篇预告defaults.lua 如何提供共享事件循环骨架,让所有脚本只需注册回调、无需各自实现 mp_event_loop


8. 自己动手:写一个最简 C 模块

如果你在自定义 mpv 构建中想加自己的 C 模块,只需要三件事:

9.1 定义函数表

c 复制代码
static const struct fn_entry my_fns[] = {
    FN_ENTRY(hello),    // script_hello 是你要实现的 C 函数
    AF_ENTRY(readdir),  // 如果需要 talloc 自动管理
    {0}
};

9.2 在 add_functions 中注册

c 复制代码
static void add_functions(struct script_ctx *ctx)
{
    lua_State *L = ctx->state;
    register_package_fns(L, "mp",       main_fns);
    register_package_fns(L, "mp.utils", utils_fns);
    register_package_fns(L, "mp.myext", my_fns);  // ← 加这一行
}

9.3 Lua 侧使用

lua 复制代码
local myext = require("mp.myext")
myext.hello()

不需要改 package.loaded、不需要写 Lua 胶水代码、不需要处理模块缓存。push_module_table 帮你全做了。


9. 结论

mpv 的 Lua 模块系统本质上是在 C 侧模拟了 require 的缓存语义。关键洞察有三个:

  1. 模块 = package.loaded 里的一个 tablepush_module_table 做的事情就是"查缓存 → 新建 → 写入缓存"这三步,和 Lua 的 require 保持完全一致的语义。

  2. register_package_fns 是一个通用的函数表注入器 。只要定义好 fn_entry 数组(用 FN_ENTRY / AF_ENTRY 宏),就能一行代码完成整个模块的 C 函数注册。

  3. C 模块和 Lua 内置脚本共用 package.loaded 缓存空间 。C 模块在 run_lua 中主动注册,Lua 脚本通过 package.preload 懒加载------两者互不干扰,且对 Lua 侧完全透明:require("mp.utils") 拿到的就是一个普通 table,你不需要关心它来自 C 还是 Lua。

这套机制的精髓在于:用 Lua 最原生的方式(table + require 缓存)承载 C 侧的能力注入,零侵入。

相关推荐
我不是懒洋洋1 小时前
从零实现一个Redis客户端:RESP协议与网络编程
开发语言·c++
玖玥拾1 小时前
C/C++ 基础笔记(六)
c语言·c++·内存管理
秋田君1 小时前
2026 前端新出路:掌握 C++ 核心语法,无缝衔接 QT 桌面开发
前端·c++·qt
handler011 小时前
【C++11 】Lambda 表达式、std::function 与 std::bind 解析
c++·c·c++11·bind·解耦·function·lamda
2023自学中2 小时前
imx6ull 开发板,RTMP 推流本地视频 到虚拟机
linux·音视频·嵌入式·开发板
字节高级特工2 小时前
C++11(二) 革新:引用折叠与lambda表达式
java·开发语言·c++·算法
知识领航员2 小时前
30个AI音乐提示词|直接复制可用,覆盖6大风格
人工智能·adobe·chatgpt·prompt·aigc·音视频
白驹笙鸣2 小时前
STL allocator作用
开发语言·c++
小小编程路2 小时前
C++ STL 原理与性能
开发语言·c++