skynet源码分析5:lua绑定之地基
2016-09-26 21:13
555 查看
前面四篇已经涵盖了skynet的c层核心,剩下的timer,socket模块本身和actor模型没什么关系,且比较独立,最后再看吧。光用skynet的c接口,是很难在这上面写业务逻辑的,所以要找一种更爽快的方式来使用。官方推荐的是lua,利用lua的协程对skynet的消息分发做了封装,使得actor之间的异步消息通信有同步一样的操作感,并且做了一些的扩展模块来方便使用。lua简洁实用的风格我个人也很钟意。
要想做一个lua binding来使用,要有两个必要条件:
根据skynet的模块契约实现一个动态库作为lua的宿主。
将skynet的公开接口封装成一个lua库。
以下用skynet-lua代指官方的lua binding
宿主
先来想一想这个宿主要干些什么?它起码要完成三件事:
创建一个lua虚拟机
加载执行一个lua服务的启动脚本
将回调的数据传递给lua层
skynet-lua的宿主的叫snlua,就是干这三件事的,其实现在/service-src/service_snlua.c中,直接来看四个契约函数吧:
create函数创建了一个lua vm,且注入了自己的分配器,为了使用jemalloc,获取vm的内存量,限制vm内存上限。
再来看看init函数:
注册了回调,向自己发送一个消息,然后什么也没做,看来真正的初始化是推迟到了第一条消息的处理上做了。
来看看回调函数吧:
初始化在init_cb,若失败就关闭自己,看看init_cb:
step1:25行前,保存sc,加载配置项(config里的lua_path,lua_cpath,lua_service等)。
step2:45行前,加载了一个lua的加载器脚本,用它来设置这些配置项,并运行入口脚本,各配置项含义如下:
lua_service:lua服务(actor)的搜索路径,与lua_path的语义一致,不占用lua本身的lua_path,但有一个小行为,当服务名不是一个文件名,而是一个目录名时,会把目录加入lua_path,也就是搜索路径为:/?/main.lua,服务名为foo,会把/foo/?.lua加入lua_path。这样为了同服务之间的脚本加载方便。服务名来自init_cb的args,args[0]为服务名,后续为入口脚本的参数.
lua_path,lua_cpath与lua本身的语义一致,加载器仅仅将它们赋给package.path,package.cpath.
lua_preload:如果指定,则在运行入口脚本前会先运行它。
之所以要用一个lua加载器来间接运行入口脚本,主要是为了实现起来方便。加载器最后还会写入两个全局变量:SERVICE_NAME(服务名),SERVICE_PATH(服务目录)
step3:看看入口脚本有没有设置memlimit(vm内存上限),如果有就设置到snlua_ud,这样vm在分配内存时就可以做判断。这个参数也只能在这个时机里设置,因为后面snlua就打酱油了,再也回不到它的领空了。
至此,一个lua服务就得以运行,入口脚本只需要利用skynet的lua api注册一次回调函数,那么就可以接管消息的处理了。
skynet的lua封装
这个封装的是c层的核心接口,实现在/lualib-src/lua_skynet.c中:
接口很少,因为c层的接口很少,就不一一叙述了。
至此,两个必要条件满足,lua binding的核心就完成了,剩下的就是如何让业务层使用起来更方便的封装了,主要针对消息分发,这也是skynet最为复杂的地方,下一篇再讲。
要想做一个lua binding来使用,要有两个必要条件:
根据skynet的模块契约实现一个动态库作为lua的宿主。
将skynet的公开接口封装成一个lua库。
以下用skynet-lua代指官方的lua binding
宿主
先来想一想这个宿主要干些什么?它起码要完成三件事:
创建一个lua虚拟机
加载执行一个lua服务的启动脚本
将回调的数据传递给lua层
skynet-lua的宿主的叫snlua,就是干这三件事的,其实现在/service-src/service_snlua.c中,直接来看四个契约函数吧:
struct snlua * snlua_create(void) { struct snlua * l = skynet_malloc(sizeof(*l)); memset(l,0,sizeof(*l)); l->mem_report = MEMORY_WARNING_REPORT; l->mem_limit = 0; l->L = lua_newstate(lalloc, l); return l; }
create函数创建了一个lua vm,且注入了自己的分配器,为了使用jemalloc,获取vm的内存量,限制vm内存上限。
再来看看init函数:
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) { int sz = strlen(args); char * tmp = skynet_malloc(sz); memcpy(tmp, args, sz); skynet_callback(ctx, l , launch_cb); const char * self = skynet_command(ctx, "REG", NULL); uint32_t handle_id = strtoul(self+1, NULL, 16); // it must be first message skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz); return 0; }
注册了回调,向自己发送一个消息,然后什么也没做,看来真正的初始化是推迟到了第一条消息的处理上做了。
来看看回调函数吧:
static int launch_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) { assert(type == 0 && session == 0); struct snlua *l = ud; skynet_callback(context, NULL, NULL); int err = init_cb(l, context, msg, sz); if (err) { skynet_command(context, "EXIT", NULL); } return 0; }
初始化在init_cb,若失败就关闭自己,看看init_cb:
static int init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) { lua_State *L = l->L; l->ctx = ctx; lua_gc(L, LUA_GCSTOP, 0); lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */ lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV"); luaL_openlibs(L); lua_pushlightuserdata(L, ctx); lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context"); luaL_requiref(L, "skynet.codecache", codecache , 0); lua_pop(L,1); const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua"); lua_pushstring(L, path); lua_setglobal(L, "LUA_PATH"); const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so"); lua_pushstring(L, cpath); lua_setglobal(L, "LUA_CPATH"); const char *service = optstring(ctx, "luaservice", "./service/?.lua"); lua_pushstring(L, service); lua_setglobal(L, "LUA_SERVICE"); const char *preload = skynet_command(ctx, "GETENV", "preload"); lua_pushstring(L, preload); lua_setglobal(L, "LUA_PRELOAD"); lua_pushcfunction(L, traceback); assert(lua_gettop(L) == 1); const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua"); int r = luaL_loadfile(L,loader); if (r != LUA_OK) { skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1)); report_launcher_error(ctx); return 1; } lua_pushlstring(L, args, sz); r = lua_pcall(L,1,0,1); if (r != LUA_OK) { skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1)); report_launcher_error(ctx); return 1; } lua_settop(L,0); if (lua_getfield(L, LUA_REGISTRYINDEX, "memlimit") == LUA_TNUMBER) { size_t limit = lua_tointeger(L, -1); l->mem_limit = limit; skynet_error(ctx, "Set memory limit to %.2f M", (float)limit / (1024 * 1024)); lua_pushnil(L); lua_setfield(L, LUA_REGISTRYINDEX, "memlimit"); } lua_pop(L, 1); lua_gc(L, LUA_GCRESTART, 0); return 0; }
step1:25行前,保存sc,加载配置项(config里的lua_path,lua_cpath,lua_service等)。
step2:45行前,加载了一个lua的加载器脚本,用它来设置这些配置项,并运行入口脚本,各配置项含义如下:
lua_service:lua服务(actor)的搜索路径,与lua_path的语义一致,不占用lua本身的lua_path,但有一个小行为,当服务名不是一个文件名,而是一个目录名时,会把目录加入lua_path,也就是搜索路径为:/?/main.lua,服务名为foo,会把/foo/?.lua加入lua_path。这样为了同服务之间的脚本加载方便。服务名来自init_cb的args,args[0]为服务名,后续为入口脚本的参数.
lua_path,lua_cpath与lua本身的语义一致,加载器仅仅将它们赋给package.path,package.cpath.
lua_preload:如果指定,则在运行入口脚本前会先运行它。
之所以要用一个lua加载器来间接运行入口脚本,主要是为了实现起来方便。加载器最后还会写入两个全局变量:SERVICE_NAME(服务名),SERVICE_PATH(服务目录)
step3:看看入口脚本有没有设置memlimit(vm内存上限),如果有就设置到snlua_ud,这样vm在分配内存时就可以做判断。这个参数也只能在这个时机里设置,因为后面snlua就打酱油了,再也回不到它的领空了。
至此,一个lua服务就得以运行,入口脚本只需要利用skynet的lua api注册一次回调函数,那么就可以接管消息的处理了。
skynet的lua封装
这个封装的是c层的核心接口,实现在/lualib-src/lua_skynet.c中:
int luaopen_skynet_core(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "send" , lsend }, { "genid", lgenid }, { "redirect", lredirect }, { "command" , lcommand }, { "intcommand", lintcommand }, { "error", lerror }, { "tostring", ltostring }, { "harbor", lharbor }, { "pack", luaseri_pack }, { "unpack", luaseri_unpack }, { "packstring", lpackstring }, { "trash" , ltrash }, { "callback", lcallback }, { "now", lnow }, { NULL, NULL }, }; luaL_newlibtable(L, l); lua_getfield(L, LUA_REGISTRYINDEX, "skynet_context"); struct skynet_context *ctx = lua_touserdata(L,-1); if (ctx == NULL) { return luaL_error(L, "Init skynet context first"); } luaL_setfuncs(L,l,1); return 1; }
接口很少,因为c层的接口很少,就不一一叙述了。
至此,两个必要条件满足,lua binding的核心就完成了,剩下的就是如何让业务层使用起来更方便的封装了,主要针对消息分发,这也是skynet最为复杂的地方,下一篇再讲。
相关文章推荐
- skynet源码分析(9)--LUA C API
- Lua源码分析(3) -- 虚拟机
- lua源码分析(局部变量的定义)
- asp.net mvc源码分析-DefaultModelBinder 集合绑定
- skynet-源码分析1:目录下的文件整理
- skynet-源码分析1:目录下的文件整理
- Lua源码分析 -- 虚拟机以及指令解释
- Lua源码分析(1) -- 简介
- jQuery源码分析-10事件处理-Event-事件绑定与删除-bind/unbind+live/die+delegat/unde
- asp.net mvc源码分析-DefaultModelBinder 自定义的普通数据类型的绑定和验证
- 详解关于Lua源码分析学习教程
- lua源码分析2(局部函数的定义)
- lua源码分析2(局部函数的定义)
- Lua源码分析(2) -- 对象表示
- LuaStudio源码分析2资源文件
- Lua源码分析(1)
- lua源码分析4(lua是怎么执行的)
- Lua源码分析(原创)[持续更新中]
- Lua源码分析之对象内存
- LUA源码分析三:table分析(1)