首页 > 其他分享 >lua closure 引用值capture及栈结构

lua closure 引用值capture及栈结构

时间:2024-04-10 14:48:43浏览次数:24  
标签:closure capture upvalue function upvalues lua stack

问题

对于习惯了C/C++的程序员来说,像lua/python这种动态语言总是有一些看起来新奇的特性。其中一个比较典型的例子就是闭包,尽管C++的lambda表达式隐约有了闭包的影子,但是相比较而言还是lua的闭包更强大:

lua的闭包可以捕捉任意存储类型(函数参数,全局i变量,局部变量)变量,并且更重要的是可以把这个闭包作为“first class”对象返回。如果用C++的视角来看,当函数调用返回后,闭包中引用的局部变量可能已经销毁或者内存已经被重用。直观的解决方法就是单独拷贝一份,如果C++的lambda都是通过value capture的,那返回lambda也没问题。

栈结构

状态机结构

一个lua虚拟机中栈相关的变量包括stack、top和stack_last三个,分别指向了栈底,栈顶和可用内存空间的结尾。结构本身和vector这类容器的内存挂历思路一样:预留/预分配一个足够大的空间,然后按需使用,避免频繁地内存申请和释放操作。

/*
** 'per thread' state
*/
struct lua_State {
  CommonHeader;
  unsigned short nci;  /* number of items in 'ci' list */
  lu_byte status;
  StkId top;  /* first free slot in the stack */
  global_State *l_G;
  CallInfo *ci;  /* call info for current function */
  const Instruction *oldpc;  /* last pc traced */
  StkId stack_last;  /* last free slot in the stack */
  StkId stack;  /* stack base */
  UpVal *openupval;  /* list of open upvalues in this stack */
  GCObject *gclist;
///...
};

StkId类型

这些字段的类型都是一个指针,但是指向的内容都足够小:不会大于一个指针的大小。这样配合类型信息,可以完整的存储整数、浮点数两种类型。


typedef TValue *StkId;  /* index to stack elements */

/*
** Tagged Values. This is the basic representation of values in Lua,
** an actual value plus a tag with its type.
*/

/*
** Union of all Lua values
*/
typedef union Value {
  GCObject *gc;    /* collectable objects */
  void *p;         /* light userdata */
  int b;           /* booleans */
  lua_CFunction f; /* light C functions */
  lua_Integer i;   /* integer numbers */
  lua_Number n;    /* float numbers */
} Value;


#define TValuefields	Value value_; int tt_


typedef struct lua_TValue {
  TValuefields;
} TValue;

使用场景

在解析字符串时,直接是通过递增指针(L->top--),这意味着栈变量的使用是连续的,栈是一个数组结构(每个元素都有相同的结构和内存大小)。

/*
** creates a new string and anchors it in scanner's table so that
** it will not be collected until the end of the compilation
** (by that time it should be anchored somewhere)
*/
TString *luaX_newstring (LexState *ls, const char *str, size_t l) {
  lua_State *L = ls->L;
  TValue *o;  /* entry for 'str' */
  TString *ts = luaS_newlstr(L, str, l);  /* create new string */
  setsvalue2s(L, L->top++, ts);  /* temporarily anchor it in stack */
  o = luaH_set(L, ls->h, L->top - 1);
  if (ttisnil(o)) {  /* not in use yet? */
    /* boolean value does not need GC barrier;
       table has no metatable, so it does not need to invalidate cache */
    setbvalue(o, 1);  /* t[string] = true */
    luaC_checkGC(L);
  }
  else {  /* string already present */
    ts = tsvalue(keyfromval(o));  /* re-use value previously stored */
  }
  L->top--;  /* remove string from stack */
  return ts;
}

官方例子

以lua官方关于closure的文档为例

    function newCounter ()
      local i = 0
      return function ()   -- anonymous function
               i = i + 1
               return i
             end
    end
    
    c1 = newCounter()
    print(c1())  --> 1
    print(c1())  --> 2

closure内部使用了局部变量i,当执行流从newCounter返回值后,对i的访问是否会像C++中的lambda引用栈变量一样引用到“脏数据”(注意:此时的变量i是一个整数,可以直接放入到栈上的一个TValue内存而不需要额外存储空间)。

实现问题

直观上考虑,应该是和C++的实现一样:发现有capture的时候自己拷贝一份。 但是还有另外两个问题:

  • 局部变量 vs 全局变量

全局变量不需要/也不能拷贝,以为直观上看closure对于全局变量的访问应该是相同的。

  • 深拷贝 vs 浅拷贝

如果栈变量是一个string,那么栈变量只是通过gc指向了TString结构。由于lua有自动垃圾回收机制,所以应该不需要深拷贝字符串内容。

lua实现

官方文档《The Implementation of Lua 5.0》的第五节“Functions and Closures”系统/准确的说明了lua的这部分实现。

When the variable goes out of scope, it migrates into a slot inside the upvalue itself (Figure 4, right). Because access is indirect through a pointer in the upvalue, this migration is transparent to any code that reads or writes the variable. Unlike its inner functions, the function that declares the variable accesses it as it accesses its own local variables: directly in the stack.

在closure生成的时候,的确是把stack中的对象拷贝一份过来。

一些细节

寻址方式

由于upvalue存储的物理位置不同,所以lua虚拟机也需要体现这种区别,就像386中栈操作使用单独的push/pop一样。在lua中访问栈变量,函数参数,常量使用的都是不同的指令模式。

从下面的反编译代码可以看到,对于upvalue的读写访问使用的是特殊的GETUPVAL/SETUPVAL指令。

tsecer@harry: cat -n luaclosure.lua 
     1      function newCounter ()
     2        local i = 0
     3        return function ()   -- anonymous function
     4                 i = i + 1
     5                 return i
     6               end
     7      end
     8
     9      c1 = newCounter()
    10      print(c1())
    11      print(c1())
    12
tsecer@harry: luac -v
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
tsecer@harry: luac -l luaclosure.lua 

main <luaclosure.lua:0,0> (14 instructions, 56 bytes at 0x15d7530)
0+ params, 2 slots, 0 upvalues, 0 locals, 3 constants, 1 function
        1       [7]     CLOSURE         0 0     ; 0x15d7710
        2       [1]     SETGLOBAL       0 -1    ; newCounter
        3       [9]     GETGLOBAL       0 -1    ; newCounter
        4       [9]     CALL            0 1 2
        5       [9]     SETGLOBAL       0 -2    ; c1
        6       [10]    GETGLOBAL       0 -3    ; print
        7       [10]    GETGLOBAL       1 -2    ; c1
        8       [10]    CALL            1 1 0
        9       [10]    CALL            0 0 1
        10      [11]    GETGLOBAL       0 -3    ; print
        11      [11]    GETGLOBAL       1 -2    ; c1
        12      [11]    CALL            1 1 0
        13      [11]    CALL            0 0 1
        14      [11]    RETURN          0 1

function <luaclosure.lua:1,7> (5 instructions, 20 bytes at 0x15d7710)
0 params, 2 slots, 0 upvalues, 1 local, 1 constant, 1 function
        1       [2]     LOADK           0 -1    ; 0
        2       [6]     CLOSURE         1 0     ; 0x15d78c0
        3       [6]     MOVE            0 0
        4       [6]     RETURN          1 2
        5       [7]     RETURN          0 1

function <luaclosure.lua:3,6> (6 instructions, 24 bytes at 0x15d78c0)
0 params, 2 slots, 1 upvalue, 0 locals, 1 constant, 0 functions
        1       [4]     GETUPVAL        0 0     ; i
        2       [4]     ADD             0 0 -1  ; - 1
        3       [4]     SETUPVAL        0 0     ; i
        4       [5]     GETUPVAL        0 0     ; i
        5       [5]     RETURN          0 2
        6       [6]     RETURN          0 1
tsecer@harry: 

数据结构

在一个Closure结构中,和函数原型(Proto)同等级别的就有一个UpVal数组,也就是Closure = Proto + upvalue。我们甚至可以把Closure理解为一个功能比C++模板更强大的“函数模板”,C++的模板函数只接受全局指针和编译常量,而lua的闭包可以接受任意变量(例如栈变量)。

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

/*
** Upvalues for Lua closures
*/
struct UpVal {
  TValue *v;  /* points to stack or to its own value */
  lu_mem refcount;  /* reference counter */
  union {
    struct {  /* (when open) */
      UpVal *next;  /* linked list */
      int touched;  /* mark to avoid cycles with dead threads */
    } open;
    TValue value;  /* the value (when closed) */
  } u;
};

upvalue的复制

在执行CLOSURE指令时触发pushclosure调用

      vmcase(OP_CLOSURE) {
        Proto *p = cl->p->p[GETARG_Bx(i)];
        LClosure *ncl = getcached(p, cl->upvals, base);  /* cached closure */
        if (ncl == NULL)  /* no match? */
          pushclosure(L, p, cl->upvals, base, ra);  /* create a new one */
        else
          setclLvalue(L, ra, ncl);  /* push cashed closure */
        checkGC(L, ra + 1);
        vmbreak;
      }

逐个upvalue调用luaF_findupval函数

/*
** create a new Lua closure, push it in the stack, and initialize
** its upvalues. Note that the closure is not cached if prototype is
** already black (which means that 'cache' was already cleared by the
** GC).
*/
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;
  setclLvalue(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];
    ncl->upvals[i]->refcount++;
    /* new closure is white, so we do not need a barrier here */
  }
  if (!isblack(p))  /* cache will not break GC invariant? */
    p->cache = ncl;  /* save it on cache for reuse */
}

在luaF_findupval函数中如果不存在则通过luaM_new创建一个新变量。

UpVal *luaF_findupval (lua_State *L, StkId level) {
  UpVal **pp = &L->openupval;
  UpVal *p;
  UpVal *uv;
  lua_assert(isintwups(L) || L->openupval == NULL);
  while (*pp != NULL && (p = *pp)->v >= level) {
    lua_assert(upisopen(p));
    if (p->v == level)  /* found a corresponding upvalue? */
      return p;  /* return it */
    pp = &p->u.open.next;
  }
  /* not found: create a new upvalue */
  uv = luaM_new(L, UpVal);
  uv->refcount = 0;
  uv->u.open.next = *pp;  /* link it to list of open upvalues */
  uv->u.open.touched = 1;
  *pp = uv;
  uv->v = level;  /* current value lives in the stack */
  if (!isintwups(L)) {  /* thread not in list of threads with upvalues? */
    L->twups = G(L)->twups;  /* link it to the list */
    G(L)->twups = L;
  }
  return uv;
}

非栈变量的处理

注意到pushclosure尝试拷贝upvalue的时候,会有一个额外的instack判断,非instack变量不会触发拷贝。这也就达到了closure中捕捉的全局变量只有一份的目的。

static int newupvalue (FuncState *fs, TString *name, expdesc *v) {
  Proto *f = fs->f;
  int oldsize = f->sizeupvalues;
  checklimit(fs, fs->nups + 1, MAXUPVAL, "upvalues");
  luaM_growvector(fs->ls->L, f->upvalues, fs->nups, f->sizeupvalues,
                  Upvaldesc, MAXUPVAL, "upvalues");
  while (oldsize < f->sizeupvalues)
    f->upvalues[oldsize++].name = NULL;
  f->upvalues[fs->nups].instack = (v->k == VLOCAL);
  f->upvalues[fs->nups].idx = cast_byte(v->u.info);
  f->upvalues[fs->nups].name = name;
  luaC_objbarrier(fs->ls->L, f, name);
  return fs->nups++;
}

C++中函数返回lambda

如果分析gcc的lambda实现的话,可以发现gcc的内部实现和lua差不多:gcc是在内部创建了一个匿名的(编译器可见)struct,struct将捕捉的参数作为成员变量,并且生成一个函数调用的operator,这两个分别对应lua中typedef struct LClosure结构的

UpVal upvals[1]; / list of upvalues */

struct Proto *p;

从下面的测试代码可以看到,c++中捕捉的变量也是拷贝了自己的一份。

tsecer@harry: cat lambda.cpp 
#include <stdio.h>
#include <string.h>

struct S
{
    int a[10];
};

auto ret_lambda()
{
    S s{1, 2, 3};

    auto local = [s]()
    {
        printf("%d, %d\n", s.a[0], s.a[1]);
    };

    memset(&s, 0 , sizeof(s));

    return local;
}

int main(int argc, const char *argv[])
{
    ret_lambda()();
    return 0;
}
tsecer@harry: g++ lambda.cpp
tsecer@harry: ./a.out 
1, 2
tsecer@harry: 

标签:closure,capture,upvalue,function,upvalues,lua,stack
From: https://www.cnblogs.com/tsecer/p/18125981

相关文章

  • openresty重定向外部的lua目录
    openresty重定向外部的lua目录背景:  目前暂定采用nginx.exe所在目录外的lua文件,因为不想把nginx.exe所在目录纳入版本控制管理,也不想添加git过滤目录。解决方案:方案1:  1.因为每个worker和master进程都需要设置lua的搜索路径,通过以下方式设置。package.path=package.pa......
  • Lua的垃圾回收
    1弱引用表1.1弱引用如果一个对象被引用,那么垃圾回收器不会回收该对象,这就是“强引用”。与“强引用”对应,如果一个对象没有被引用,或者仅被“弱引用”,那么垃圾回收器会忽视弱引用,回收该对象。1.2弱引用表指元素均被“弱引用”的表。我们无法通过变量直接“弱引用”一个对象......
  • ETL工具-nifi干货系列 第九讲 处理器EvaluateJsonPath,根据JsonPath提取字段
    1、其实这一节课本来按照计划一起学习RouteOnAttribute处理器(相当于java中的ifelse,switchcase控制语句),但是在学习的过程中遇到了一些问题。RouteOnAttribute需要依赖处理器EvaluateJsonPath,所以本节课我们一起来学习下EvaluateJsonPath处理器。如下图所示: 本节课的示例依然......
  • 【智能排班系统】基于Redis的increment命令和lua脚本实现IP限流
    文章目录什么是IP限流?为什么需要IP限流?保护服务器资源防范恶意攻击与滥用提升用户体验IP限流方式:实现实现原理代码结构lua脚本为什么要使用lua脚本Ip限流工具类对所有接口进行限流IP限流请求过滤器SpringSecurity中配置过滤器对不经过SpringSecurity的过滤器的请求进......
  • RobotJS截取屏幕screen.capture踩坑
    RobotJS截取屏幕screen.capture踩坑 调用robot.screen.capture()或robot.screen.capture(0,0,1920,1080),返回的Bitmap对象是色彩格式是BGR色彩,这导致了如果未经处理就直接生成图像,色彩会产生错误,只需将BGR色彩转换成RGB色彩即可。constrobot=require('robotjs');con......
  • 【STM32嵌入式系统设计与开发】——16InputCapture(输入捕获应用)
    这里写目录标题STM32资料包:百度网盘下载链接:链接:https://pan.baidu.com/s/1mWx9Asaipk-2z9HY17wYXQ?pwd=8888提取码:8888一、任务描述二、任务实施1、工程文件夹创建2、函数编辑(1)主函数编辑(2)USART1初始化函数(usart1_init())(3)USART数据发送函数(USART1_Send_Data())(4)USART......
  • lua的协程
      lua协程的创建通常是通过coroutine.create(f),其中f就是协程的主体程序,它必须是一个函数。coroutine.create返回一个类型为thread(lua的8种内建类型之一)的变量。------Createsanewcoroutine,withbody`f`.`f`mustbeaLuafunction.Returns---thisnewcorouti......
  • Redis+lua脚本配合AOP限流
    限流Redis脚本限流脚本配合切面注解定义注解:@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic@interfaceRateLimiter{/***限流key*/publicStringkey()defaultCacheConstants.RATE_LIMIT_KEY;/**......
  • clion + EmmyLua插件实现Lua的本地调试
    安装好EmmyLua插件后,它会提供两种调试方式:1.使用mobdebug调试2.使用EmmyDebugger调试。mobdebug是用lua写的一个调试模块,基于网络通信(所以依赖luasocket),以字符形式传递调试命令和返回结果。所以使用mobdebug前先需要安装luasocket模块。由于基于网络,所以支持跨机器调试,本机调试......
  • love 2d Lua 俄罗斯方块超详细教程
    源码已经更新在CSDN的码库里:gitclonehttps://gitcode.com/funsion/love2d-game.git一直在找Lua能快速便捷实现图形界面的软件,找了一堆,终于发现love2d是小而美的原生lua图形界面实现的方式。并参考相关教程做了一个更详细的,以便入门。功能如上图,开发过程用了love2d,......