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

lua vm 五: upvalue

前言

在 lua vm 中,upvalue 是一个重要的数据结构。upvalue 以一种高效的方式实现了词法作用域,使得函数能成为 lua 中的第一类值,也因其高效的设计,导致在实现上有点复杂。

函数 (proto) + upvalue 构成了闭包(closure),在 lua 中调用一个函数,实际上是调用一个闭包。upvalue 就相当于函数的上下文。

这种带 “上下文” 的函数,也导致了热更新的麻烦,可以说是麻烦透顶了。没法简单的通过替换新的函数代码来更新一个旧闭包,因为旧闭包上可能带着几个 upvalue,这几个 upvalue 的值可能已经发生改变,或者也被其他的函数引用着。


图1:函数与upvalue

所以,要更新一个旧闭包,得把旧闭包上的所有 upvalue 都找出来,绑定到新函数上,形成一个新闭包,再用这个新闭包替换旧闭包。

本文主要讲 upvalue 在 lua vm 中的实现,下篇文章再讲如何解决带有 upvalue 的闭包的热更新问题。

下文分析基于 lua5.4.6。


1. upvalue


1.1 upvalue 实现上要解决的问题

upvalue 就是外部函数的局部变量,比如下面的函数定义中,var1 就是 inner 的一个 upvalue。

local function getf(delta)local var1 = 100local function inner()return var1+deltaendreturn inner
endlocal f1 = getf(10)

upvalue 复杂的地方在于,在离开了 upvalue 的作用域之后,还要能够访问得到。比如上面调用了 local f1 = getf(10)var1 是在 getf 的栈上分配的,getf 返回后,栈空间被抹掉,但 inner 还要能访问 var1,所以要想办法把它捕捉下来。


1.2 upvalue 的实现

下面先讲 lua 闭包的 upvalue,最后再讲 c 闭包的,因为复杂性几乎都在 lua 闭包这里面了。


1.2.1 upvalue 相关的结构体

与 upvalue 相关的结构体有:

1、UpVal,可以说是 upvalue 的本体了,很巧妙的结构,运行时用到的变量。

typedef struct UpVal {CommonHeader;union {TValue *p;  /* points to stack or to its own value */ptrdiff_t offset;  /* used while the stack is being reallocated */} v;union {struct {  /* (when open) */struct UpVal *next;  /* linked list */struct UpVal **previous;} open;TValue value;  /* the value (when closed) */} u;
} UpVal;

2、Upvaldesc,这个是编译时产生的信息,Proto 结构体就包含 Upvaldesc* 类型的数组:upvalues,用于描述当前函数用到的 upvalue 信息。

typedef struct Upvaldesc {TString *name;  /* upvalue name (for debug information) */lu_byte instack;  /* whether it is in stack (register) */lu_byte idx;  /* index of upvalue (in stack or in outer function's list) */lu_byte kind;  /* kind of corresponding variable */
} Upvaldesc;typedef struct Proto {...Upvaldesc *upvalues;  /* upvalue information */...
} Proto;

3、lua_State 中的 openupval 字段,它是 UpVal* 类型的链表,它相当于一个 cache,保存当前栈上还存活着的被引用到的 upvalue。

struct lua_State {...UpVal *openupval;  /* list of open upvalues in this stack */...
};

4、LClosure 中的 upvals 数组。

typedef struct LClosure {ClosureHeader;struct Proto *p;UpVal *upvals[1];  /* list of upvalues */
} LClosure;

1.2.2 upvalue 的访问

upvalue 是间接访问的,LClosure 结构体的 upvals 字段是 UpVal* 类型的数组。访问的时候先通过 upvals 获得到 UpVal 指针,再通过 UpVal 里面的 v.p 去访问具体的变量,伪码如下:

UpVal* UpValPtr = closure->upvals[upidx];
TValue* p = UpValPtr->v.p;

需要这样间接访问,主要是因为 UpVal 本身会随着函数调用的返回发生状态的变化:从 open 改为 close,这时它的值也从栈上被拷贝到了 “自己身上”,所以指针(v.p)是变化的,不能写死。

至于为什么会发生 open 到 close 的变化,后面会讲。


1.2.3 upvalue 的创建

upvalue 是在编译的时候计算好一个 Proto 需要什么 upvalue,相关信息存放在 Proto 的 upvalues 数组( Upvaldesc *upvalues; /* upvalue information */)中的。

举个例子,对于这样一个脚本,内部的函数 f1、f2 既引用了 getf 之外的变量 var1,也引用了 getf 之内的变量 var2、var3,并且在 local f1, f2 = getf() 调用完成后,f1 还要能访问到 var1、var2,f2 还要能访问到 var1、var3。


local var1 = 1local function getf()local var2 = 2local var3 = 3local function f1()return var1 + var2endlocal function f2()return var1 + var3endreturn f1, f2
endlocal retf1, retf2 = getf()

编译结果是:


图2:upvalue 编译信息

从编译结果可以看到,每个 Proto 都会生成 UpvalueDesc 数组,用于描述这个函数(proto)会用到的 upvalue。

index 表示在 LClosure 的 upvals 数组中是第几个。
name 表示变量名。
instack 表示这个 upvalue 是否刚好是上一层函数的局部变量,比如 var2 是 f1 的上一层的,所以 instack 为 true,而 var1 是上两层的,所以为 false。
idx 表示 instack 为 false 的情况下,可以在上一层函数的 upvals 数组的第几个找到这个 upvalue。
kind 表示 upvalue 类型,一般都是 VDKREG,即普通类型。


补充说明,kind 是 lua5.4 才整出来的,lua5.3 及之前都只有 VDKREG。5.4 新增了 RDKCONST,RDKTOCLOSE,RDKCTC。

RDKCONST 是对应到 <const>,指定变量为常量。
RDKTOCLOSE 是对应到 <close>,指定变量为 to be closed 的(类似于 RAII 特性,超出作用域后执行 __close 元函数)。
RDKCTC 我也闹不清楚。


从例子上可以看到,f1 引用了上一层函数 getf 的局部变量 var2,所以它的 instack 值是 true,而引用了上两层的局部变量 var1,则它的 instack 是 false。

instack 主要就是在创建 Closure 的时候帮助初始化 Closure 的 upvals 数组,对于 instack 为 true 的 upvalue,直接搜索上一层函数的栈空间即可,对于 instack 为 false 的 upvalue,就不能这样了,为什么呢?因为上两层的有可能已经不在栈上了。能想象得到吗?举个例子:

local function l1()local var1 = 1local function l2()local var2 = 2local function l3()return var1+var2+3endreturn l3endreturn l2
endlocal ret_l2 = l1()local ret_l3 = ret_l2()

调用 l1 的时候,得到了 l2,这时候 l1 已经返回了,它的栈已经回收了,这时候再调用 l2,在创建 l3 这个闭包的时候,是不可能再找到 l1 的栈去搜索 var1 这个变量的。

所以,要解决这个问题,就需要让 l2 在创建的时候,先帮忙把 var1 捕捉下来保存到自己的 upvals 数组中,等 l3 创建的时候,就可以从 l2 的 upvals 数组中找到了。

这正是 pushclosure 干的活:

static void pushclosure (lua_State *L, Proto *p, UpVal **encup, StkId base,StkId ra) {int nup = p->sizeupvalues;Upvaldesc *uv = p->upvalues;int i;LClosure *ncl = luaF_newLclosure(L, nup);ncl->p = p;setclLvalue2s(L, ra, ncl);  /* anchor new closure in stack */for (i = 0; i < nup; i++) {  /* fill in its upvalues */if (uv[i].instack)  /* upvalue refers to local variable? */ncl->upvals[i] = luaF_findupval(L, base + uv[i].idx);else  /* get upvalue from enclosing function */ncl->upvals[i] = encup[uv[i].idx];luaC_objbarrier(L, ncl, ncl->upvals[i]);}
}

函数实现可以看到,instack 为 true 时,调用 luaF_findupval 去上一层函数的栈上搜索,instack 为 false 时,上一层函数已经帮忙捕捉好了,直接从它的 upvals 数组(即这里的 encup 变量中)索引。

这里 uv[i].idx 就是上面 upvaldesc 的 idx 列,即当 instack 为 false 时,它对应于上一层函数的 upvals 数组的第几项。


1.2.4 upvalue 的变化:从 open 到 close

分两个阶段讲,getf 调用时以及 getf 调用后。

1、getf 调用时,var2、var3 这两个变量作为 f1, f2 的 upvalue,它们还处在 getf 的栈上,这时候它们会被放在 lua_State 的 openupval 链表中。

2、getf 调用后,它的栈要被收回的,这时候 lua vm 会调用 luaF_close 来关闭 getf 栈上被引用的 upvalue,最终是 luaF_closeupval 这个函数执行:

void luaF_closeupval (lua_State *L, StkId level) {UpVal *uv;StkId upl;  /* stack index pointed by 'uv' */while ((uv = L->openupval) != NULL && (upl = uplevel(uv)) >= level) {TValue *slot = &uv->u.value;  /* new position for value */lua_assert(uplevel(uv) < L->top.p);luaF_unlinkupval(uv);  /* remove upvalue from 'openupval' list */setobj(L, slot, uv->v.p);  /* move value to upvalue slot */uv->v.p = slot;  /* now current value lives here */if (!iswhite(uv)) {  /* neither white nor dead? */nw2black(uv);  /* closed upvalues cannot be gray */luaC_barrier(L, uv, slot);}}
}

要理解这个函数,就要知道 StkId level 这个参数的意义,它在这里是 getfbase 指针,即它的栈底。同个 lua_State 的函数调用链上的所有函数共用一个栈,按顺序各占一段栈空间,栈是一个数组,所以后调用的函数的变量在栈上的索引是更大的,表现上就是指针值更大。而 openupval 链表里面 Upval 里的 p 就是指向这指针,所以遍历 openupval 的时候,遇到 p 比 base 大的,就表明这个是 getf 栈上的变量,要把它 close 掉。

close 的操作就是把 upval 从 openupval 链表移掉,同时把 upval 的 p 指向的值拷贝到它自身上。


图3:upvalue close 时的拷贝


1.2.5 C 闭包中的 upvalue

C 闭包(CClosure)也是有 upvalue 的,是在 lua_pushcclosure 时设置的,但用的是值拷贝,所以多个 C 闭包不能共享 upvalue。如果要在多个 C 闭包,只能是各自的upvalue 指向同一个 table 这样的变量。

CClosure 的 upvalue 直接用的是 TValue 类型的数组(不是指针),在创建的时候用的值拷贝。

typedef struct CClosure {ClosureHeader;lua_CFunction f;TValue upvalue[1];  /* list of upvalues */
} CClosure;

2. 参考

相关文章:

lua vm 五: upvalue

前言 在 lua vm 中&#xff0c;upvalue 是一个重要的数据结构。upvalue 以一种高效的方式实现了词法作用域&#xff0c;使得函数能成为 lua 中的第一类值&#xff0c;也因其高效的设计&#xff0c;导致在实现上有点复杂。 函数 (proto) upvalue 构成了闭包&#xff08;closu…...

React Native中集成ArcGIS以显示地图、渲染自定义图层和获取地理信息数据

在您的数据采集上传的应用中集成ArcGIS以显示地图、渲染自定义图层和获取地理信息数据是一项常见需求。下面是如何实现这些功能的详细指南&#xff0c;包括具体步骤和示例代码。 1. 显示地图 原生开发 Android&#xff1a; 使用ArcGIS Android SDK。您需要在AndroidManifest…...

java中的异常-异常处理(try、catch、finally、throw、throws)+自定义异常

一、概述 1、java程序员在编写程序时提前编写好对异常的处理程序&#xff0c;在程序发生异常时就可以执行预先设定好的处理程序&#xff0c;处理程序执行完之后&#xff0c;可以继续向后执行后面的程序 2、异常处理程序是在程序执行出现异常时才执行的 二、5个关键字 1、tr…...

深入了解反射

newInstance 可访问性限制&#xff1a; newInstance()方法只能调用无参的公共构造函数。如果类没有无参公共构造函数&#xff0c;那么newInstance()方法将无法使用。 异常处理&#xff1a; newInstance()方法在创建对象时会抛出受检异常InstantiationException和IllegalAcces…...

5、搭建前端项目

5.1 使用vite vue搭建 win r 打开终端 切换到你想要搭建的盘 npm init vitelatest跟着以下步骤取名即可 cd fullStackBlognpm installnpm run dev默认在 http://localhost:5173/ 下启动了 5.2 用vscode打开项目并安装需要的插件 1、删除多余的 HelloWorld.vue 文件 2、安装…...

LLM之Agent初探

Agent是什么&#xff1f; Agent一词起源于拉丁语中的Agere&#xff0c;意思是“to do”。在LLM语境下&#xff0c;Agent可以理解为在某种能自主理解、规划决策、执行复杂任务的智能体。 Agent并非ChatGPT升级版&#xff0c;它不仅告诉你“如何做”&#xff0c;更会帮你去做。…...

目录穿越漏洞CVE-2018-7171复现 又学到一招小技巧!!!!

还是半夜睡不着&#xff0c;打开靶机开始操作。今天看了文件下载和目录穿越漏洞想结合以及防御方法。半夜来进行操作一波。复现一下漏洞&#xff0c;这个网上的文章页比较的少&#xff01;&#xff01;&#xff01; 开始操作起来&#xff01;&#xff01;&#xff01; 进入到页…...

代码随想录算法训练营day41

题目&#xff1a;01背包理论基础、416. 分割等和子集 参考链接&#xff1a;代码随想录 动态规划&#xff1a;01背包理论基础 思路&#xff1a;01背包是所有背包问题的基础&#xff0c;第一次看到比较懵&#xff0c;完全不知道dp数据怎么设置。具体分析还是dp五部曲&#xff…...

从0~1开发财务软件

1.获取图形验证码接口 功能要求 1、随机生成6位字符 2、将字符生成base64位格式的图片&#xff0c;返回给前端 3、将生成的字符存储到redis中&#xff0c;用匿名身份id&#xff08;clientId&#xff09;作为key&#xff0c;验证码作为value。 clientId通过/login/getClien…...

Python实现连连看9

&#xff08;2&#xff09;标识选中的图片 在判断出玩家选中的是哪一张图片之后&#xff0c;接下来就可以标识选中的图片了&#xff0c;即在该选中的图片外围画矩形。代码如下所示。 FIRSTCLICK True #FIRSTCLICK是全局变量 if(click_col>0 and click_row>0) and \(no…...

项目验收总体计划书(实际项目验收原件参考Word)

测试目标&#xff1a;确保项目的需求分析说明书中的所有功能需求都已实现&#xff0c;且能正常运行&#xff1b;确保项目的业务流程符合用户和产品设计要求&#xff1b;确保项目的界面美观、风格一致、易学习、易操作、易理解。 软件全套文档过去进主页。 一、 前言 &#xff0…...

C++基础与深度解析 | 异常处理 | 枚举与联合 | 嵌套类与局部类 | 嵌套名字空间与匿名名字空间 | 位域与volatile关键字

文章目录 一、异常处理二、枚举与联合三、嵌套类与局部类四、嵌套名字空间与匿名名字空间五、位域与volatile关键字 一、异常处理 异常处理用于处理程序在调用过程中的非正常行为。 传统的处理方法&#xff1a;传返回值表示函数调用是否正常结束。 例如&#xff0c;返回 0 表示…...

番外篇 | 利用华为2023最新Gold-YOLO中的Gatherand-Distribute对特征融合模块进行改进

前言:Hello大家好,我是小哥谈。论文提出一种改进的信息融合机制Gather-and-Distribute (GD) ,通过全局融合多层特征并将全局信息注入高层,以提高YOLO系列模型的信息融合能力和检测性能。通过引入MAE-style预训练方法,进一步提高模型的准确性。🌈 目录 🚀1.论文解…...

python记录之字符串

在Python中&#xff0c;字符串是一种非常常见且重要的数据类型&#xff0c;用于存储文本信息。下面&#xff0c;我们将对Python字符串进行深入的讲解&#xff0c;包括其基本操作、常见方法、格式化以及高级特性。 1. 字符串的创建 在Python中&#xff0c;字符串可以通过单引号…...

Elasticsearch 认证模拟题 - 15

一、题目 原索引 task1 的字段 title 字段包含单词 The&#xff0c;查询 the 可以查出 1200 篇文档。重建 task1 索引为 task1_new&#xff0c;重建后的索引&#xff0c; title 字段查询 the 单词&#xff0c;不能匹配到任何文档。 PUT task1 {"mappings": {"…...

g++ 预处理 编译 汇编 链接 命令

g 预处理 编译 汇编 链接 命令 在命令行中使用 g 预处理、编译、汇编和链接源代码文件通常遵循以下步骤&#xff1a; 预处理&#xff08;Preprocessing&#xff09;&#xff1a;将源代码文件转换为经过预处理器处理的中间文件。 g -E source.cpp -o source.i 编译&#xff…...

计算机视觉中的low-level与 high-level任务

文章目录 low-level任务high-level任务区别联系others参考在计算机视觉领域中,low-level任务和high-level任务是两个重要的概念,他们分别涉及图像处理和分析的不同的层次。 low-level任务 low-level任务主要关注的是图像的底层特征,如颜色、纹理、边缘、形状等。通常涉及对…...

安徽京准NTP时钟系统:GPS北斗卫星授时下的生活重塑

安徽京准NTP时钟系统&#xff1a;GPS北斗卫星授时下的生活重塑 安徽京准NTP时钟系统&#xff1a;GPS北斗卫星授时下的生活重塑 时间的流逝自古以来时钟都是人类生活与活动的基础。然而&#xff0c;随着科技的进步&#xff0c;我们对时间管理和测量的方法已经发生了翻天覆地的变…...

图论第8天

685.冗余连接II 这题需要考虑两种情况&#xff1a; 1.两个输入 2.没有两个输入就是有成环 class Solution { public:static const int N 1005;int father[N];int n;void init(){for (int i 0; i < n; i){father[i] i;}}int find(int x){return x father[x] ? x : f…...

Python怎么配置环境变量:深度探索与实战指南

Python怎么配置环境变量&#xff1a;深度探索与实战指南 在Python编程的世界中&#xff0c;环境变量的配置是一个至关重要的步骤。它不仅影响着Python解释器的运行&#xff0c;还关系到各种第三方库和工具的使用。本文将带你深度探索如何配置Python的环境变量&#xff0c;并为…...

KubeSphere 容器平台高可用:环境搭建与可视化操作指南

Linux_k8s篇 欢迎来到Linux的世界&#xff0c;看笔记好好学多敲多打&#xff0c;每个人都是大神&#xff01; 题目&#xff1a;KubeSphere 容器平台高可用&#xff1a;环境搭建与可视化操作指南 版本号: 1.0,0 作者: 老王要学习 日期: 2025.06.05 适用环境: Ubuntu22 文档说…...

Vue记事本应用实现教程

文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展&#xff1a;显示创建时间8. 功能扩展&#xff1a;记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

超短脉冲激光自聚焦效应

前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应&#xff0c;这是一种非线性光学现象&#xff0c;主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场&#xff0c;对材料产生非线性响应&#xff0c;可能…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

零基础设计模式——行为型模式 - 责任链模式

第四部分&#xff1a;行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习&#xff01;行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想&#xff1a;使多个对象都有机会处…...

select、poll、epoll 与 Reactor 模式

在高并发网络编程领域&#xff0c;高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表&#xff0c;以及基于它们实现的 Reactor 模式&#xff0c;为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。​ 一、I…...

Linux --进程控制

本文从以下五个方面来初步认识进程控制&#xff1a; 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程&#xff0c;创建出来的进程就是子进程&#xff0c;原来的进程为父进程。…...

Yolov8 目标检测蒸馏学习记录

yolov8系列模型蒸馏基本流程&#xff0c;代码下载&#xff1a;这里本人提交了一个demo:djdll/Yolov8_Distillation: Yolov8轻量化_蒸馏代码实现 在轻量化模型设计中&#xff0c;**知识蒸馏&#xff08;Knowledge Distillation&#xff09;**被广泛应用&#xff0c;作为提升模型…...

MySQL JOIN 表过多的优化思路

当 MySQL 查询涉及大量表 JOIN 时&#xff0c;性能会显著下降。以下是优化思路和简易实现方法&#xff1a; 一、核心优化思路 减少 JOIN 数量 数据冗余&#xff1a;添加必要的冗余字段&#xff08;如订单表直接存储用户名&#xff09;合并表&#xff1a;将频繁关联的小表合并成…...

为什么要创建 Vue 实例

核心原因:Vue 需要一个「控制中心」来驱动整个应用 你可以把 Vue 实例想象成你应用的**「大脑」或「引擎」。它负责协调模板、数据、逻辑和行为,将它们变成一个活的、可交互的应用**。没有这个实例,你的代码只是一堆静态的 HTML、JavaScript 变量和函数,无法「活」起来。 …...