你有没有好奇过:mpv 启动时,Lua 脚本里写
local utils = require("mp.utils")就能拿到readdir、split_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 的缓存语义。关键洞察有三个:
-
模块 =
package.loaded里的一个 table 。push_module_table做的事情就是"查缓存 → 新建 → 写入缓存"这三步,和 Lua 的require保持完全一致的语义。 -
register_package_fns是一个通用的函数表注入器 。只要定义好fn_entry数组(用FN_ENTRY/AF_ENTRY宏),就能一行代码完成整个模块的 C 函数注册。 -
C 模块和 Lua 内置脚本共用
package.loaded缓存空间 。C 模块在run_lua中主动注册,Lua 脚本通过package.preload懒加载------两者互不干扰,且对 Lua 侧完全透明:require("mp.utils")拿到的就是一个普通 table,你不需要关心它来自 C 还是 Lua。
这套机制的精髓在于:用 Lua 最原生的方式(table + require 缓存)承载 C 侧的能力注入,零侵入。