Lua5.4 源码剖析——虚拟机2 之 闭包与UpValue
故事将由我们拥有了一段 Lua 代码开始,我们先用 Lua 语言写一段简单的底部底部打印一加一计算结果的 Lua 代码,并把代码保存在 luatest.lua 文件中:
可执行的源码源码一个 Lua 文件或者一份单独的文本形式 Lua 代码,在 Lua 源码中叫做 "Chunk"。全局全局无论我们通过什么形式去执行,底部底部或者用什么编辑器去执行,源码源码android终端源码最终为了先载入这段 Lua 的全局全局 Chunk 到内存中,无外乎会归结到以下两种方式:1)Lua 文件的底部底部载入:require 函数 或 loadfile 函数;2)Lua 文本代码块的载入:load 函数;这两种方式最终都会来到下面源码《lparse.c》luaY_parser 函数。该函数是源码源码解析器的入口函数,负责完成代码解析工作,全局全局最终会创建并返回一个 Lua 闭包(LClosure),底部底部见下图的源码源码红框部分:
另外,上图中间有一行代码最终会调用到 statement 函数,全局全局statement 函数是底部底部 Chunk 解析的核心函数,它会一个一个字符地处理我们编写的源码源码 Lua 代码,完成词法分析和语法分析工作,想要了解字符处理整个状态流程的可以自行研读该部分源码,见源码《lparse.c》statement 函数部分代码:
完成了解析工作之后,luaY_parser 函数会把解析的所有成果放到 Lua 闭包(LClosure)对象之中,这些存储的内容能保证后续执行器能正常执行 Lua 闭包对应的代码。
Lua 闭包由 Proto(也叫函数原型)与 UpValue(也叫上值)构成,见源码《lobject.h》LClosure 定义,我们下面将进行详细的web在线象棋源码讲解:
UpValue 是 Lua 闭包数据相关的,在 Lua 的函数调用中,根据数据的作用范围可以把数据分为两种类型:1)内部数据:函数内部自己定义的数据,或者通过函数参数的形式传入的数据(在 Lua 中通过参数传入的数据本质上也是先赋值给一个局部变量);2)外部数据:在函数的更外层进行定义,脱离了该函数后仍然有效的数据;外部数据在我们的 Lua 闭包中就是 UpValue,也叫上值。
既然 Lua 支持函数嵌套,也知道了 UpValue 本质就是上层函数的内部数据。那么 UpValue 有必要存储于 Lua 闭包(LClosure)结构体当中吗?是为了性能考虑而做的一层指针引用缓存吗?回答:并不是基于性能的考虑,因为在实际的 Lua 运用场景中,函数嵌套的层数通常来说不会太多,个别函数多一层的查询访问判断不会带来过多的性能开销。需要在闭包当中存储 UpValue 主要原因是因为内存。Lua 作为一门精致小巧的脚本语言,设计初衷不希望占用过多的系统内存,它会尽量及时地清理内存中用不到的对象。在嵌套函数中,内层函数如果仍然有被引用处于有效状态,而外层函数已经没有被引用了已经无效了,此时 Lua 支持在保留内层函数的情况下,对外层函数进行清除,从而可以清理掉外层函数引用的非当前函数 UpValue 用途以外的大量数据内存。
尽管外层函数被清除了,电影预告网站源码Lua 仍然可以保持内层函数用到的 UpValue 值的有效性。UpValue 如何能继续保持有效,我们在之前的基础教程《基本数据类型 之 Function》里面学习过,主要是因为 UpValue 有 open 与 close 两种状态,当外层函数被清除的时候,UpValue 会有一个由 open 状态切换到 close 状态的过程,会对数据进行一定的处理,感兴趣的同学可以回到前面复习一下。
UpValue 有效性例子
接下来我们举一个代码例子与一个图例,表现一下 UpValue 在退出外层函数后仍然生效的情况,看一下可以做什么样的功能需求,加深一下印象,请看代码与注释:
上述代码在执行 OutFunc 函数后,外层的 globalFunc 函数变量完成了赋值,每次对它进行调用,都将可以对它引用的 UpValue 值即 outUpValue 变量进行正常加 1。
函数的内部数据属于函数自身的内容,外部其它函数无法通过直接的方式访问其它函数的内部数据。函数自身的东西会存在于 LClosure 结构体的 Proto*p 字段中。Proto 全称 "Function Prototypes",通常也可以叫做 "函数原型",我们来看一下它的大象移除id源码定义,见源码《lobject.h》Proto 结构体:
结构体字段比较多,我们先不细看,后面用到哪个字段会再进行补充说明。函数的内部数据分为常量与变量(即函数局部变量),分别对应上图的如下字段:
1)常量:TValue* k 为指针指向常量数组;int sizek 为函数内部定义的常量个数,也即常量数组 k 的元素个数。
2)局部变量:LocVar* locvars 为指针指向局部变量数组;int sizelocvars 为函数定义的局部变量个数,也即局部变量数组 locvars 的元素个数。
UpValue 的描述信息会存储在 Proto 结构体中的 Upvaldesc* upvalues 字段,解析器解析 Lua 代码的时候会生成这个 UpValue 描述信息,并用于生成指令,而执行器运行的时候可以通过该描述信息方便快速地构建出真正的 UpValue 数组。
至此,我们知道了函数拥有 UpValue,有常量,有局部变量。外部数据 UpValue 也讲完,内部数据也讲完。接下来,我们开始学习函数运行的逻辑指令相关内容。
函数逻辑指令存储于函数原型 Proto 结构体中,这些函数逻辑是QQ登录端源码由一行行的 Lua 代码构成的,代码会被解析器翻译成 Lua 虚拟机能识别的指令,我们把这些指令称为 "OpCode",也叫 "操作码"。Proto 结构体存储 OpCode 使用的是下图中红框部分字段,见源码《lobject.h》Proto 结构体:
至此,我们可以简单提前说一下 Lua 虚拟机的功能了,本质上来看,Lua 虚拟机的工作,就是为当前函数(或者当前一段 OpCode 数组)准备好数据,然后有序执行 OpCode 指令。
对 OpCode 有了一定的认识了,接下来我们要补充一个 OpCode 相关的 Lua 闭包相关的内容,就是 Lua 闭包的运行环境。
一个 Lua 文件在载入的时候会先创建出一个最顶层(Top level)的 Lua 闭包,该闭包默认带有一个 UpValue,这个 UpValue 的变量名为 "_ENV",它指向 Lua 虚拟机的全局变量表,即_G 表,可以理解为_G 表即为当前 Lua 文件中代码的运行环境 (env)。事实上,每一个 Lua 闭包它们第一个 UpValue 值都是_ENV。
ENV 的定义在我们之前提到的解析器相关函数 mainfunc 中,见源码《lparser.c》:
如果想要设置这个载入后的初始运行环境不使用默认的 _G 表,除了直接在该文件代码中重新赋值_ENV 变量这种粗暴且不推荐的方式以外,通常是通过我们前面提到的加载 Lua 文件函数或加载 Lua 字符串代码函数传入 env 参数(Table 类型),就可以用自定义的 Table 作为当前 Lua 闭包的全局变量环境了,env 参数为上面两个函数的最末尾一个参数,'[' 与 ']' 字符中的内容表示参数可选,函数的定义摘自 Lua5.4 官网文档:
所以我们可以在 Lua 代码通过 _ENV 访问当前环境:
在 Lua 的旧版本中,变量的查询最多会分为 3 步:1)先从函数局部变量中进行查找;2)找不到的话就从 UpValue 中查找;3)还找不到就从全局环境默认 _G 表查找。而在 Lua5.4 中,把 UpValue 与全局 _G 表的查询统一为 UpValue 查询,并把一些操作判断提前到了解析器解析阶段进行,例如函数内部使用的某个 UpVaue 变量在代码解析的时候就可以通过 UpValue 描述信息知道存储于 Lua 闭包 upvals 数组的哪个下标位置,在执行器运行的时候只需要直接在数组拿取对应下标的这个 UpValue 数据即可。
从 OpCode 的层面来看,Lua 除了支持通过一个 UpValue 数组下标访问一个 UpValue 变量,在把 _G 表合并到 UpValue 之后,Lua 为此实现了通过一个字符串 key 值从某个 Table 类型的 UpValue 中查询变量的操作。
至此,我们了解了 Lua 闭包的结构与运行环境,以及 OpCode 的基本概念。接下来,我们将深入学习 OpCode,掌握 OpCode 就掌握了整个 Lua 虚拟机数据与逻辑的流向。
开源即时通讯GGTalk源码剖析之:客户端全局缓存及本地存储
继上篇详细介绍了 GGTalk 内置的虚拟数据库,本文将深入探讨 GGTalk 客户端的全局缓存及本地存储机制。对于还没有获取GGTalk源码的朋友,文章底部附有下载链接。
一. GGTalk 客户端缓存设计
核心在于ClientGlobalCache类,它在内存中保存用户和群组数据。此类接受泛型参数TUser和TGroup,且限定TUser和TGroup需实现特定接口,还继承自BaseGlobalCache类。三个私有字段分别用于存储用户、群组和缓存信息。
构造函数接收五个参数,用于初始化私有字段,并调用父类BaseGlobalCache的Initialize方法,实现缓存初始化逻辑。
二. GGTalk 客户端本地持久化存储
BaseGlobalCache类中,originUserLocalPersistence字段负责本地文件存储。它包含四个属性,代表好友列表、群组列表、快捷回复列表和最近联系人/群列表。
Load和Save方法用于读写本地文件,将数据存入或从文件加载。在了解本地缓存的核心概念后,回到Initialize方法,读取本地文件数据,缓存到内存中。
三. 更新本地缓存
在用户登录或断线重连时,系统会比较本地缓存与服务器数据,更新缺失或过时的信息。当缓存中只有用户自己时,会从服务器加载所有联系人;当存在其他数据时,会更新本地缓存以反映服务器最新状态。
四. 总结
GGTalk客户端缓存流程包括读取本地缓存、从服务器加载更新数据,以及在窗口关闭时将当前用户数据缓存。下篇将解析消息收发及处理机制。
敬请期待:《GGTalk 开源即时通讯系统源码剖析之:消息收发及处理》。底部链接提供下载GGTalk源码。
Navigation包 Global_planner全局路径规划源码详细解析
学习总结,如有错误欢迎指正!一丶plan_node.cpp从程序入口开始,首先在plan_node.cpp的main函数中,初始化了全局路径规划器。
costmap_2d::Costmap2DROS?lcr("costmap",?buffer);global_planner::PlannerWithCostmap?pppp("planner",?&lcr);在函数PlannerWithCostmap中设置了两种调用makePlan的路径:
PlannerWithCostmap::PlannerWithCostmap(string?name,?Costmap2DROS*?cmap)?:GlobalPlanner(name,?cmap->getCostmap(),?cmap->getGlobalFrameID())?{ ros::NodeHandle?private_nh("~");cmap_?=?cmap;make_plan_service_?=?private_nh.advertiseService("make_plan",?&PlannerWithCostmap::makePlanService,?this);pose_sub_?=?private_nh.subscribe<rm::PoseStamped>("goal",?1,?&PlannerWithCostmap::poseCallback,?this);}1.通过make_plan服务
req.start.header.frame_id?=?"map";req.goal.header.frame_id?=?"map";bool?success?=?makePlan(req.start,?req.goal,?path);2.通过goal回调函数
//得到当前机器人在MAP中的位置cmap_->getRobotPose(global_pose);makePlan(global_pose,?*goal,?path);在getRobotPose函数中,通过tf_.transform(robot_pose, global_pose, global_frame_);函数,默认将机器人pose从base_link转换到map坐标系下,可通过参数设置。得到起始点和目标点传入到makePlan中。
二丶 planner_core.cpp//register?this?planner?as?a?BaseGlobalPlanner?pluginPLUGINLIB_EXPORT_CLASS(global_planner::GlobalPlanner,?nav_core::BaseGlobalPlanner)global_planner 是基类nav_core :: BaseGlobalPlanner的一个插件子类
首先在构造函数中需要初始化GlobalPlanner,在initialize中对一些参数进行赋值。
GlobalPlanner::GlobalPlanner(std::string?name,?costmap_2d::Costmap2D*?costmap,?std::string?frame_id)?:GlobalPlanner()?{ //initialize?the?plannerinitialize(name,?costmap,?frame_id);}当调用makePlan时,首先就是判断是否已经被初始化:
//?code?line?~?makePlan()if?(!initialized_)?{ ROS_ERROR("This?planner?has?not?been?initialized?yet,?but?it?is?being?used,?please?call?initialize()?before?use");return?false;}m初始化完成之后,清除之前规划的Plan,以防万一。然后检查起点和终点是否在我们所需要的坐标系下,一般在map系下。
//clear?the?plan,?just?in?case?,?code?line??makePlan()plan.clear();if?(goal.header.frame_id?!=?global_frame)?{ ...}if?(start.header.frame_id?!=?global_frame){ ...}将世界坐标系的点(map 坐标系)转换成图像坐标系(图像左下角)下的点(以像素表示)
if?(!costmap_->worldToMap(wx,?wy,?goal_x_i,?goal_y_i))?{ ROS_WARN_THROTTLE(1.0,"The?goal?sent?to?the?global?planner?is?off?the?global?costmap.?Planning?will?always?fail?to?this?goal.");return?false;}在Costmap2D和GlobalPlanner中都有实现worldToMap,其实都是一样的,在GlobalPlanner中则需要通过调用Costmap2D来获取局部地图的起始点和分辨率,而在Costmap2D则可以直接使用全局变量。
bool?Costmap2D::worldToMap(double?wx,?double?wy,?unsigned?int&?mx,?unsigned?int&?my)?const{ ?if?(wx?<?origin_x_?||?wy?<?origin_y_)return?false;?mx?=?(int)((wx?-?origin_x_)?/?resolution_);?my?=?(int)((wy?-?origin_y_)?/?resolution_);?if?(mx?<?size_x_?&&?my?<?size_y_)return?true;?return?false;}old_navfnbehavior ?作为一种旧式规划行为:
The start of the path does not match the actual start location.
The very end of the path moves along grid lines.
All of the coordinates are slightly shifted by half a grid cell
现在在worldToMap所使用的convert_offset_ = 0
接下来将机器人Robot所在的位置,在costmap中设置成free,当前位置不可能是一个障碍物。 即在clearRobotCell()函数中:mx,my即当前机器人位置。
PlannerWithCostmap::PlannerWithCostmap(string?name,?Costmap2DROS*?cmap)?:GlobalPlanner(name,?cmap->getCostmap(),?cmap->getGlobalFrameID())?{ ros::NodeHandle?private_nh("~");cmap_?=?cmap;make_plan_service_?=?private_nh.advertiseService("make_plan",?&PlannerWithCostmap::makePlanService,?this);pose_sub_?=?private_nh.subscribe<rm::PoseStamped>("goal",?1,?&PlannerWithCostmap::poseCallback,?this);}0设置规划地图边框:outlineMap,此函数由参数outline_map_决定。 根据costmap跟起始终止点计算网格的potential,计算的算法有两种:Dijkstra和A*,具体算法便不再讨论,资料很多。 当提取到plan之后,调用getPlanFromPotential,把path转换变成geometry_msgs::PoseStamped数据类型。
PlannerWithCostmap::PlannerWithCostmap(string?name,?Costmap2DROS*?cmap)?:GlobalPlanner(name,?cmap->getCostmap(),?cmap->getGlobalFrameID())?{ ros::NodeHandle?private_nh("~");cmap_?=?cmap;make_plan_service_?=?private_nh.advertiseService("make_plan",?&PlannerWithCostmap::makePlanService,?this);pose_sub_?=?private_nh.subscribe<rm::PoseStamped>("goal",?1,?&PlannerWithCostmap::poseCallback,?this);}1此时便得到所需要的路径plan,最终调用OrientationFilter平滑之后发布出去。
PlannerWithCostmap::PlannerWithCostmap(string?name,?Costmap2DROS*?cmap)?:GlobalPlanner(name,?cmap->getCostmap(),?cmap->getGlobalFrameID())?{ ros::NodeHandle?private_nh("~");cmap_?=?cmap;make_plan_service_?=?private_nh.advertiseService("make_plan",?&PlannerWithCostmap::makePlanService,?this);pose_sub_?=?private_nh.subscribe<rm::PoseStamped>("goal",?1,?&PlannerWithCostmap::poseCallback,?this);}22024-11-30 12:22
2024-11-30 12:05
2024-11-30 11:41
2024-11-30 11:27
2024-11-30 09:54