lua(9)-模块和自定义loader

在实际的项目中,往往不会只有一个.lua文件,而是有多个.lua文件一起在跑,这就需要宿主程序在开始运行的时候把所有需要用到的.lua文件全部加载,这一步通常是由require代为完成。

通常程序里第一个被加载的.lua文件会作为入口文件,由这个入口文件来require其他.lua文件,从而实现对所有.lua文件的加载。

 

lua官方提供了require、module函数来加载多个lua文件,这里就讲解require函数(module在后续的lua版本中已经被弃用,不做讲解)。

一个lua文件可以看作是一个模块,require函数的作用就是加载一个模块,使用require函数时,需要传入模块的名字,比如需要加载A.lua,即写成require "A"。为了更便捷地了解require函数,我们可以先去lua的官网下载lua的源代码,下载完毕解压后,能在里面看到许多的.h和.c文件。




  • require的作用:

 加载给定的模块

  • require函数的原形为:

require(modname)

  • require函数对应ll_require,ll_require的实现在loadlib.c中:
static const luaL_Reg ll_funcs[] = {
    {"module", ll_module},
    {"require",ll_require},
    {NULL, NULL}
};
static int ll_require (lua_State *L) {
       const char*name = luaL_checkstring(L, 1);
       int i;
       lua_settop(L,1); /* _LOADED table will be at index 2 */
       lua_getfield(L,LUA_REGISTRYINDEX, "_LOADED");
       lua_getfield(L,2, name);
       if(lua_toboolean(L, -1)) { /* is it there? */
              if(lua_touserdata(L, -1) == sentinel) /* check loops */
                     luaL_error(L,"loop or previous error loading module " LUA_QS, name);
              return1; /* package is already loaded */
       }
       /* elsemust load it; iterate over available loaders */
       lua_getfield(L,LUA_ENVIRONINDEX, "loaders");
       if (!lua_istable(L,-1))
              luaL_error(L,LUA_QL("package.loaders") " must be a table");
       lua_pushliteral(L,""); /* error message accumulator */
       for (i=1;; i++) {
              lua_rawgeti(L,-2, i); /* get a loader */
              if(lua_isnil(L, -1))
                     luaL_error(L,"module " LUA_QS " not found:%s",
                     name,lua_tostring(L, -2));
              lua_pushstring(L,name);
              lua_call(L,1, 1); /* call it */
              if(lua_isfunction(L, -1)) /* did it find module? */
                     break;/* module loaded successfully */
              elseif (lua_isstring(L, -1)) /* loader returned error message? */
                     lua_concat(L,2); /* accumulate it */
              else
                     lua_pop(L,1);
       }
       lua_pushlightuserdata(L,sentinel);
       lua_setfield(L,2, name); /* _LOADED[name] = sentinel */
       lua_pushstring(L,name); /* pass name as argument to module */
       lua_call(L,1, 1); /* run loaded module */
       if (!lua_isnil(L,-1)) /* non-nil return? */
              lua_setfield(L,2, name); /* _LOADED[name] = returned value */
       lua_getfield(L,2, name);
       if(lua_touserdata(L, -1) == sentinel) { /* module did not set a value? */
              lua_pushboolean(L,1); /* use true as result */
              lua_pushvalue(L,-1); /* extra copy to be returned */
              lua_setfield(L,2, name); /* _LOADED[name] = true */
       }
       return 1;
}
  • require函数的执行流程:
  1. require(modname)函数会在开始的时候会在lua的虚拟机(栈)中查找package.loaded表的内容以确认modname是否已经被加载,如果是,require返回存储在package.loaded[modname]中的值;
  2. 否则,它会尝试为该模块寻找一个加载器;
  3. 加载器就是加载这个模块的接口,由于require支持加载.c、.dll、.lua等多种文件,因此其源码中也提供了多个接口函数来分别实现加载这些文件,这些接口函数就是加载器;
  4. require从package.loaders这个表中来查找加载器,package.loaders表存放的都是函数指针,这些函数指针就是加载器。
  • package.loaders的声明和实现在loadlib.c中:
static const lua_CFunction loaders[] =
{loader_preload, loader_Lua, loader_C, loader_Croot,NULL};
  1. require首先从package.loaders表中取出loader_preload;
  2. loader_preload函数从package.preload表中查找package.preload[modname],package.preload[modname]存放的也是一个函数指针,如果它不为空,这个函数指针就是一个加载器,使用这个加载器加载文件;
  3. 否则,require将会从package.loaders表中取出loader_Lua,loader_Lua方法则会从package.path表中存储的路径中查找.lua模块,如果查找成功,则加载这个模块;
  4. 如果在package.path中查找失败了,require将会从package.loaders表中取出loader_C,loader_C方法会从package.cpath表中存储的路径查找模块;
  5. 如果还是查找失败,require会尝试一个一体化加载器,即loader_Croot函数,loader_Croot是一个一体化加载器,也会从package.cpath中查找模块,而且能加载a.b.c这种文件,如果使用loader_C来加载a.b.c文件,它会为a查找一个C库,而loader_Croot会为a、b和c分别查找一个C库,查找完毕后,将这几个C子模块打包进一个库中。

对require函数有了初步的了解后,我们来尝试使用它将多个.lua文件加载进程序块中,让它们能相互调用。

 

方法1,使用package.path:

还是新建一个lua1.cpp,这回加载放在F盘LuaFile文件夹里的mainLua.lua

lua1.cpp:

// lua1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "iostream"
#include "stdio.h"
#include "string.h"
//lua.h和lualib.h作为C代码运行,在C++文件中使用lua.h和lualib.h,需使用extern命令实现混合编程,否则会提示无法解析外部函数
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
using namespace std;
#pragma comment(lib,"lua5.1.lib")
 
int _tmain(int argc, _TCHAR* argv[])
{
       charbuff[256];
       int error;
       //创建一个lua状态机(虚拟栈),这个状态机没有包含预定义的lua库函数
       lua_State*L = luaL_newstate();
       //打开标准库,这样状态机可以使用lua库函数,可以解释lua脚本
       luaL_openlibs(L);
       charfileName[] = "F:/LuaFile/mainLua.lua";
       //加载lua文件,解释器将lua文件的内容读取出来,动态编译为代码块
       luaL_loadfile(L,fileName);
       //执行编译好的代码块,参数1为栈指针,参数2为传给待调用函数的参数数量,
       //参数3为期望返回的结果的数量,参数4为错误处理函数的索引
       int result= lua_pcall(L,0,LUA_MULTRET,0);
       //如果运行没有错误,lua_pcall将返回0
       if(!result)
       {
              printf_s("lua脚本运行成功\n");
       }
       lua_close(L);
 
       return 0;
}

LuaFile文件夹目录如下





运行cpp程序,结果如下


能发现程序将package.path和"mainLua"打印出来了,但是并没有打印"mainLua2"和"mainLua3",说明mainLua2.lua和mainLua3.lua文件没有被成功加载。

package.path包含着当前vs程序所在的目录、lua的安装目录,lua的require方法会从这个package.path中查找目标文件,并将"?"号替代成文件名。

比如mainLua.lua中require "mainLua2",那么require方法将会从

    .\mainLua2.lua

    F:\VSProject\lua1\Debug\lua\mainLua2.lua

    F:\VSProject\lua1\Debug\lua\mainLua2\init.lua

    F:\VSProject\lua1\Debug\mainLua2.lua

    F:\VSProject\lua1\Debug\mainLua2\init.lua

    D:\5.1\lua\mainLua2.luac

    mainLua2.lua

这几个目录中查找mainLua2.lua文件,如果没有找到,将捕获异常。mainLua2.lua此时的目录是F:\LuaFile\mainLua2.lua,因此程序没有找到文件。

我们来把mainLua.lua的内容改一下。


注意"\"是转义字符,如果在代码中用"\"表示路径,用两个"\"表示,或者用"/"代替。修改后,运行程序,就能发现程序已经找到了mainLua2.lua与mainLua3.lua文件。


使用这种方法来查找lua文件比较简便,但是不易于扩展,如果程序规模逐渐加大,需要引入其他目录的lua文件,需要频繁地修改package.path或需要在require后加上较为冗余的路径字段,比如现在多了F:\LuaFile2和F:\LuaFile\LuaFile3这两个目录,需求在程序中引入这两个目录中的所有lua文件,那mainLua.lua文件可能得这样改:



这样看不论哪一种都很繁琐而且不好维护,我们来看第二种关联lua的方法。


方法2,使用自定义的loader:

之前提到package.loaders中的loader_Lua方法是加载.lua模块的加载器,这个加载器会从package.path中查找.lua文件,我们可以把这个loader_Lua加载器给替换掉,使用自己的加载器来查找给定目录下的.lua文件。

首先我们准备几个.lua文件





然后我们将这四个.lua文件放置在几个不同的目录中,这里的示例目录是:

    F:\LuaFile\mainLua.lua

    ‪F:\LuaFile\mainLua2.lua

    F:\LuaFile\LuaFile3\mainLua3.lua

    F:\LuaFile2\mainLua4.lua





然后写一个.cpp文件:

#include "stdafx.h"
#include "iostream"
#include "stdio.h"
#include "string.h"
#include "windows.h"
#include "vector"
//lua.h和lualib.h作为C代码运行,在C++文件中使用lua.h和lualib.h,需使用extern命令,否则会提示无法解析外部函数
extern "C"
{
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
}
using namespace std;
#pragma comment(lib,"lua5.1.lib")
 
vector<string> luaPaths;
 
 
string luaName; //当前require的lua文件名
string luaFile; //当前require的lua的完整路径
bool isFind; //是否查找成功
 
char* WCHAR2Char(WCHAR* wc)
{
       char* c;
       int len=WideCharToMultiByte(CP_ACP,0,wc,wcslen(wc),NULL,0,NULL,NULL);
       c=new char[len+1];
       WideCharToMultiByte(CP_ACP,0,wc,wcslen(wc),c,len,NULL,NULL);
       c[len]='\0';
       return c;
}
 
//遍历目标目录及其子目录下的所有文件
void searchFile(const char path[], int level = 0)
{
       if(isFind)
              return;
 
       charfind_path[128];
       sprintf_s(find_path,"%s*", path);
 
       WIN32_FIND_DATAFindFileData;
       HANDLEhFind;
       BOOLbContinue = TRUE;
 
       //char*转LPCWSTR(constWCHAR*)
       WCHARwszClassName[256];
       memset(wszClassName,0,sizeof(wszClassName));
       MultiByteToWideChar(CP_ACP,0,find_path,strlen(find_path)+1,wszClassName,
              sizeof(wszClassName)/sizeof(wszClassName[0]));
       hFind =FindFirstFile(wszClassName, &FindFileData);
       if (hFind== INVALID_HANDLE_VALUE)
              return;
       while(bContinue)
       {
              if(_stricmp(WCHAR2Char(FindFileData.cFileName), "..") &&_stricmp(WCHAR2Char(FindFileData.cFileName), "."))
              {
                     strings(WCHAR2Char(FindFileData.cFileName));
                     //如果当前文件就是require的文件,停止搜索
                     if(0== strcmp(s.c_str(),luaName.c_str()))
                     {
                            luaFile= path;
                            luaFile.append(luaName);
                            isFind= true;
                            return;
                     }
                     if(FindFileData.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY)
                     {
                            sprintf_s(find_path,"%s%s/", path, WCHAR2Char(FindFileData.cFileName));
                            searchFile(find_path,level + 1);
                     }
              }
              bContinue= FindNextFile(hFind, &FindFileData);
       }
}
 
int myLoader(lua_State * L)
{
       isFind = false;
       luaFile = "";
 
       //为文件追加.lua后缀
       stringfileName(luaL_checkstring(L, 1));
       fileName.append(".lua");
       //记录当前要查找的.lua文件的文件名
       luaName =fileName;
 
       for(inti=0;i<luaPaths.size();i++)
       {
              if(isFind)
                     break;
              stringluaFilePath = luaPaths[i];
              //luaFilePath.append(fileName);
              searchFile(luaFilePath.c_str());
       }
       if(0 !=luaL_loadfile(L,luaFile.c_str()))
       {
              printf_s("loadlua file error.");
       }
 
       //返回的结果数
       return 1;
}
 
//把package.loaders这个table第二个元素替换成myLoader
void addLuaLoader(lua_State * m_state, lua_CFunction func)
{
       if (!func)return;
 
       // stackcontent after the invoking of the function
       // getloader table(获得lua预加载好的package这个table,并将这个table压桟)
       lua_getglobal(m_state,"package"); /* L: package */
       // 获得package这个table里面的loaders元素,并将其压桟,这个loaders也是一个table,table里的元素全是方法
       lua_getfield(m_state,-1, "loaders"); /* L: package, loaders */
       // insertloader into index 2(将自己的loader方法压桟)
       lua_pushcfunction(m_state,func); /* L: package, loaders, func */
       // 遍历loaders这个table,将其下标为2的元素替换为自己的loader方法
       for (int i= lua_objlen(m_state, -2) + 1; i > 2; --i)
       {
              lua_rawgeti(m_state,-2, i - 1); /* L: package, loaders, func, function */
              //we call lua_rawgeti, so the loader table now is at -3
              lua_rawseti(m_state,-3, i); /* L: package, loaders, func */
       }
       lua_rawseti(m_state,-2, 2); /* L: package, loaders */
       // setloaders into package
       lua_setfield(m_state,-2, "loaders"); /* L: package */
       lua_pop(m_state,1);
 
}
 
int _tmain(int argc, _TCHAR* argv[])
{
       charbuff[256];
       int error;
       //创建一个lua状态机(虚拟栈),这个状态机没有包含预定义的lua库函数
       lua_State*L = luaL_newstate();
       //打开标准库,这样状态机可以使用lua库函数,可以解释lua脚本
       luaL_openlibs(L);
 
       luaPaths.clear();
       //自定义的loader将从这两个目录去查找.lua文件
       luaPaths.push_back("F:/LuaFile/");
       luaPaths.push_back("F:/LuaFile2/");
 
       //==================使用自定义的loader加载lua文件======================
       addLuaLoader(L,myLoader);
       //=====================================================================
 
 
       charfileName[] = "F:/LuaFile/mainLua.lua";
       加载lua文件,解释器将lua文件的内容读取出来,动态编译为代码块
       luaL_loadfile(L,fileName);
       //执行编译好的代码块,参数1为栈指针,参数2为传给待调用函数的参数数量,
       //参数3为期望返回的结果的数量,参数4为错误处理函数的索引(这个索引为压入栈的函数索引,0表示没有错误处理函数)
       int result= lua_pcall(L,0,LUA_MULTRET,0);
       //如果运行没有错误,lua_pcall将返回0
       if(!result)
       {
              printf_s("lua脚本运行成功\n");
       }
       lua_close(L);
 
       return 0;
}


程序执行结果:


可以看到,程序已经成功把这四个.lua文件加载到了程序块中,它们之间可以相互调用彼此提供的接口。

上面的cpp程序的执行流程为:

  1. 在c程序中创建lua虚拟机(栈)
  2. 将可能要存储.lua文件的根目录保存在一个数组中,也就是luaPaths,luaPaths初始化完毕后,后面程序将会从"F:/LuaFile/"与"F:/LuaFile2/"这两个目录中查找.lua文件。这一步可以修改为读取配置文件,从配置文件中获取文件目录。
  3. 使用自定义loader。addLuaLoader(lua_State* m_state,lua_CFunction func)这个函数的作用是将栈中package.loaders这个表里的第二个元素(即上文提到的loader_Lua函数指针)替换成func。
  4. 使用luaL_loadfile方法从mainLua.lua文件中加载代码块,并将代码块压入栈中。
  5. main函数中执行lua_pcall方法,执行当前栈中的代码块。
  6. 代码块第一句被执行的语句是print("mainLua");接下来执行require"mainLua2",由于原loader方法已经被替换成myLoader,因此执行myLoader方法。
  7. 在myLoader方法中,使用luaL_checkstring方法获取当前虚拟机中执行的方法的参数,也就是require的参数,即“mainLua2”这个字符串,然后为这个字符串追加.lua后缀。接下来开始使用searchFile方法来循环遍历luaPaths数组中的目录,查找luaPaths[i]中以及其子目录的mainLua2.lua文件,如果查找成功,使用luaL_loadfile方法加载这个文件。加载完毕后,立即执行加载完毕的代码块,也就是print("mainLua2")。
  8. 继续执行require"mainLua3" 和require "mainLua4",步骤同上。
  9. 结束。

参考资料:http://hmanager.gw.com.cn/ID_DTS_315300.html

http://bbs.csdn.net/topics/330134613

http://blog.csdn.net/zhouxuguang236/article/details/8761497

 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值