This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
<audio id="audio" title="第19讲 | 如何嵌入脚本语言?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7f/48/7fea12ffdf335e92000c5730677ee148.mp3"></audio>
从2005年开始逐渐流行使用C/C++语言结合脚本语言Lua、Python、Ruby等等编写游戏。这是因为用C/C++编写游戏的传统方式,硬代码太多,而使用硬代码编写的游戏,更新难度很大,除非重新编译一次程序。
于是,就有人开始使用配置文件来做活动逻辑。比如填写好配置表、玩家等级多少、攻击力如何、等于多少的伤害力等等,一开始就将这些内容都读取进代码,在游戏中实时计算出来。
但是这种方法其实也并不方便。很久以前的游戏由于硬件资源限制所以一般都加载WAV格式。而加载MP3则需要机器对音乐文件进行解压缩再播放如果机器硬件计算能力不好的话会由于解压缩而导致整个游戏的运行效率下降。
脚本语言也是如此如果机器硬件能力不好的话会由于脚本语言的虚拟机要解释程序导致游戏运行效率下降。随着电脑硬件的提升我们在游戏中加载MP3音乐文件成为可能而在游戏中加载脚本语言进行逻辑编写当然也是可以的。
《魔兽世界》就是使用Lua脚本语言编写的。类似《GTA》等大型游戏都拥有一套自己的脚本语言和体系。 **使用脚本语言,是为了能够在编写硬代码的同时,也能很方便地、不需要重新编译地编写逻辑代码。** 事实上现在很多大型游戏都使用这种方式来编写代码甚至一些游戏引擎本身也支持脚本语言和引擎本身所提供的语言分离编写。比如引擎用C++语言编写脚本语言用Lua编写。
## 为什么使用Lua脚本嵌入C/C++硬代码?
今天我就来教你使用Lua脚本来嵌入C/C++硬代码。为什么我要选择Lua脚本语言来编写代码呢
因为**Lua脚本足够轻量级几乎没有冗余的代码。Lua虚拟机的执行效率几乎可以媲美C/C++的执行效率**。如果选择Python、Ruby等常用脚本语言来嵌入并不是不行而是要付出执行效率作为代价。因为Python、Ruby的执行效率远逊于Lua。
如果没有非常多的编码经验你可能会问为什么Python、Ruby的执行效率远逊于Lua呢这个问题用一本书的篇幅恐怕才能彻底讲明白。我这里只简要说一下原因。
Lua的虚拟机很简单指令设计得也精简Lua本身是基于寄存器的虚拟机实现而Python等其他脚本语言是基于堆栈的虚拟机而基于寄存器的虚拟机字节码更简单、高效。因为字节码一般会同时包含指令、操作数、操作目标等内容。
另一方面Python、Ruby之所以应用范围广是因为它们拥有大量的成熟库和框架而Lua只是一种很纯粹的脚本语言。因为Lua没有过多的第三方库只提供最基础的I/O处理、数学运算处理、字符串处理等别的与操作系统相关度密切的例如网络、多线程、音频视频处理等等都不提供。
我在[第6讲](https://time.geekbang.org/column/article/8782)里已经非常详细地讲过如何将Lua脚本编译成为静态库如果不记得的话可以回去复习一下。编译好静态库liblua.a之后我们就可以在编程中使用它了。
你也可以选择在解压缩出来的目录内使用make命令来直接编译编译会生成Lua虚拟机的执行文件lua.exe、luac.exe当然这需要一整套MinGW的环境支持。
开始我们还是使用MinGW Development Studio来创建一个工程。由于只是示例所以名字可以任意取。我取一个叫作lua_test的工程名并且将工程设置为Win32 Console Application。你可以看这个示例图。<br>
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/c2/51/c214aaccf9b6cd231d73304beea8ba51.jpg">
建立好了工程之后我们新建一个test.c文件。这个文件位于lua源代码路径下。我们将liblua.a 文件也放到同一个目录下,以方便后续链接时候调用。
在包含Lua头文件之前我们需要将头文件写在某一个.hpp文件下以便一次性包含进去我们的代码可以这么写。
```
#ifdef __CPLUSPLUS
extern &quot;C&quot; {
#endif
#include &quot;src/lua.h&quot;
#include &quot;src/lualib.h&quot;
#include &quot;src/lauxlib.h&quot;
#ifdef __CPLUSPLUS
}
#endi
```
你可以看到这里面包含了三个代码。这三个代码来自src目录下其中最后一个lauxlib.h包含了大量的C语言形式的接口以及扩展接口。而定义extern "C"的意思是使用C的方式进行链接前置条件是你的语言是C++语言ifdef __CPLUSPLUS
定义好了这个hpp文件后我们可以在C或者C++语言中进行包含。
```
#include “lua.hpp”
```
## 你需要了解三个Lua语言的细节问题
写完定义之后我们就可以开始对Lua进行一系列的绑定操作了。在编程之前我先用一些你能看得懂的语言对Lua语言的细节进行一些描述。有三个点需要你着重记一下。
首先,**Lua的下标都是以1为最初始的值**(当然反向可以使用-1为下标而不是我们所熟悉的0。有个传言说是因为作者当时编写最初版本的Lua时计算错误才导致的所以就这么一直沿用下来了这个说法虽然不可考但也算是一种解释。
其次在C/C++内嵌Lua的做法中**Lua有两种读取脚本的方法。**
<li>
一种方式是**读取后直接运行调用的函数是luaL_dofile**。使用这个函数,脚本会在读取完毕后直接运行。当然如果出现错误,你也不知道错误的具体位置在哪里,调试起来不是很方便。
</li>
<li>
第二种方式是**将脚本代码压到栈顶然后使用pcall操作运行脚本这个函数叫luaL_loadfile**。事实上第一种方式也是使用这种方式并且将pcall操作直接调用起来第一种方式的代码一看你就能明了。
</li>
```
#define luaL_dofile(L, fn) \
(luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))
```
这行代码在lauxlib.h中能找到。这段代码写得非常精妙它的意思是如果loadfile成功那么就运行pcall函数中间这个 || 或者已经直接判断了loadfile是否成功。因为loadfile函数操作成功就返回0否则就返回1。
而在“或者”这个逻辑判断下只要是0就继续往下判断只要是1就直接返回条件为真。所以在这行代码下只要是1就中断dofile这个宏的操作只要是0就进行pcall操作。
最后,我要说一下**Lua的堆栈**。理解了堆栈的计数方式就能很容易地理解我后续要讲解的代码中的计数方式。Lua的堆栈可以从这个图里看出来从栈底往上表示可以用1、2、3、4、5而从栈顶往下表示是-1、-2、-3、-4、-5。<br>
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/af/ad/af00612d1f227cac1900b5c9e153c6ad.jpg">
## 如何使用Lua以及liblua.a来进行与C语言的绑定操作
我们现在开始使用Lua以及liblua.a来进行与C语言的绑定操作。
首先我们需要包含之前我们所定义的lua.hpp头文件随后我们开始在main入口函数处定义一些变量。
```
#include &quot;lua.hpp&quot;
int main(int argc, char ** argv)
{
int r;
const char* err;
lua_State* ls;
….
}
```
在这里我们定义了三个变量其中r是用来接收返回值的err是一个常量字符串用来接收错误字符串并打印出来而lua_State* ls就是Lua虚拟机的指针了。
我们再来看接下来的代码。
```
ls = luaL_newstate();
luaL_openlibs(ls);
```
在这两行代码中首先初始化一个虚拟机在Lua 5.1中,使用的函数是 lua_open来新建虚拟机并且将虚拟机地址赋值给ls指针。随后我们拿到这个指针之后就在之后的代码中“打开”Lua所需要用到的各种库。我们用到luaL_openlibs。我现在只是给你示范你可以一个一个库单独打开。
我们新建了虚拟机并且打开了Lua类库。我们继续看下面的代码。
```
r = luaL_loadfile(ls, argv[1]);
if(r)
{
err = lua_tostring(ls, -1);
if(err)
printf(&quot;err1: %s\n&quot;, err);
return 1;
}
r = lua_pcall(ls, 0, 0, 0);
if(r)
{
err = lua_tostring(ls, -1);
if(err)
printf(&quot;err2: %s\n&quot;, err);
return 1;
}
lua_close(ls);
```
我来具体解释一下。这段代码中argv[1]的是命令行输入的第一个内容。比如我们的程序叫lua_test那么我们在Windows命令行中输入lua_test a.lua那么其中a.lua 就是argv[1] 这个内容。
luaL_loadfile我们在前面介绍过就是载入文件并不运行。当然在这个期间它会检查基础的语法。如果你少一个括号或者多一个引号就会在这个时候给你一个错误信息这个错误信息就是利用r这个变量判断的。如果r的返回值不等于0的话那就是出错了。出错的时候Lua会将出错信息压栈顶而栈顶是从-1开始表示所以我们要取出栈顶的错误信息lua_tostring(ls, -1);并且将它赋值给err最后由err打印出来。
认为没有错误之后就是过了这一关。第二关我们需要使用lua_pcall函数来调用Lua脚本文件其中第一个参数是虚拟机指针第二个参数是传递多少参数给Lua第三个参数是这个脚本返回多少值第四个是错误处理函数可以是0那就是无处理函数。
pcall的返回值也是一样如果不是0的话就说明出错了。和之前的luaL_loadfile不同这时候一般是运行时错误比如运行时类型错误等等。同样的pcall也会把错误信息压到栈顶我们直接去将栈顶的内容转成string就可以打印出来了。最后我们将Lua虚拟机通过lua_close关闭。
按常理来说我们现在可以来运行一下效果了你可以先等等我们先写一段错误的Lua代码来看看执行起来会发生什么情况。
```
print &quot;test running&quot;)
```
我们故意少写一个括号,然后将源代码命名为 a.lua我们来运行看看。会出现一个这样的错误信息<br>
<img style="margin: 0 auto" src="https://static001.geekbang.org/resource/image/cf/2a/cfad6d423a3c95bacba12b5e8dc3782a.jpg">
在发现语法错误后程序就会报错另外如果你输入了一个根本不存在的文件比如我们这么运行test_lua xxx.lua也会在loadfile的时候出错。
## 小结
我们今天的内容就到这里。下次我会进一步把Lua的脚本嵌入的细节呈现在你面前。我们来总结一下今天的内容。
<li>
因为Lua脚本足够轻量级几乎没有冗余的代码。Lua虚拟机的执行效率几乎可以媲美C/C++的执行效率。所以我们选择使用Lua脚本来嵌入C/C++硬代码。
</li>
<li>
Lua脚本在C/C++语言里面嵌入,需要先声明一个虚拟机并且赋值给指针。
</li>
<li>
Lua脚本需要先loadfile再pcall调用脚本文件loadfile会检查最基本的脚本文件内容比如文件是否存在比如脚本代码是否出错而pcall会在运行时出错的时候将错误压至栈顶。
</li>
<li>
Lua错误会将错误压制栈顶我们要取出来需要使用-1下标取出栈顶的内容并转成string打印。
</li>
给你留一个小问题吧。
如果直接使用luaL_dofile相对于把loadfile和pcall分开写这样有什么优劣呢
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,166 @@
<audio id="audio" title="第20讲 | 脚本语言在游戏开发中有哪些应用?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/de/3b/dee9c9b0ac13b8adef126d53d397b53b.mp3"></audio>
上一次我们谈到了如何在游戏中嵌入脚本语言我们用的语言是Lua。Lua语言具有轻量级、速度快的特点而且API的调用也非常方便和直观。现在我们仍然拿Lua脚本试着把它应用在我们开发的游戏中。
我们使用C语言来对Lua脚本的绑定做一次深入的剖析然后来看一下在游戏开发中绑定了脚本语言后脚本语言能做些什么事情。
首先,我们要明白一点,事实上**任何模块都可以使用脚本语言编写**。当然在游戏开发的过程中,需要分工明确,如果不分工的话,效率可能会比较低。
在需要某些效率要求非常高的情况下一般是用C、C++或ASM语言将底层模块搭建好然后将一些逻辑部分分出来给脚本语言处理。比如我们熟知的服务器端可以使用C/C++来编写服务器端的IOCP或者epoll处理而接收、发送、逻辑处理等等都可以使用绑定脚本的方式编写。
我们在编写的过程中需要对C/C++的语言和代码有个了解,我们需要先考虑这个函数。
```
int test_func(lua_State *L)
{
return 0;
}
```
这只是一个空的C函数在这个函数里面我们看到它的传入参数是lua_State接受一个指针L。随后这个函数返回一个0。
lua_State是Lua虚拟机的对象指针也就是我们需要把前面new出来的一个虚拟机传进去才可以保证在这个函数里面使用的是一致的虚拟机。
这个函数的作用是,**只要注册到了Lua虚拟机里面它就是lua的一个函数其中在lua函数中传入的参数由函数内部决定**。
比如我可以这么写:
```
int test_func(lua_State *L)
{
const char *p1 = lua_tostring(L, 1);
const char *p2 = lua_tostring(L, 2);
// .... do something
lua_pushstring(L, &quot;something&quot;);
return 1;
}
```
这里面lua_tosting 就是这个函数的传入参数,传入的是一个字符串的参数;第二个参数也是字符串参数,其中 lua_tosting 的第二个参数1或者2表明的是在Lua虚拟机的堆栈中从栈底到栈顶开始计数一般先压入的参数在第一个后压入的在第二个以此类推。返回1的意思是这个函数会返回一个参数这个参数就是我们前面 lua_pushstring 后压入的这个内容something这就是返回的参数。
那么这个函数究竟怎么注册成为Lua函数呢我们来看这段代码。
```
lua_register(L, &quot;test&quot;, &amp;test_func);
```
lua_register函数的功能是注册C函数到Lua虚拟机。其中L是虚拟机指针。这个在前面的代码都有说到而第二个参数test就是注册在Lua虚拟机中的函数名所以这个函数名叫test。第三个参数是函数指针我们把test_func这个函数传入到lua_register函数中。这样一个函数就注册好了。
那么如果我们在游戏中有许多许多的函数需要注册到Lua中那么这种写法是不是太慢了有没有一种快捷的写法来支持注册等操作呢
如果你没有C/C++的语言基础或者C/C++语言基础比较薄弱下面的内容可能需要花一点时间消化我也会竭尽所能解释清楚代码的意思但如果你已经是个C/C++程序员,那么下面的代码对你来说应该不会太难。
我们需要使用lua_register我们先看它里面有什么参数。第一个是**字符串**,也就是**char*<strong>第二个是**函数指针</strong>,也就是**int (**)(lua_State**)** 这种形式的。
那么我们需要定义一个struct结构这个结构可以这么写
```
#define _max 256
typedef struct _ph_func
{
char ph_name[_max];
int (*ph_p_func)(lua_State*);
} ph_func;
```
我们定义了一个struct结构这个结构的名字叫_ph_func名字叫什么并没有关系但是最开始有一个typedef这说明在这个结构声明完后接下来最后一行ph_func就是替代最初定义的那个_ph_func的名字替代的结果是**ph_func 等同于struct _ph_func**这在很多C语言的代码里面经常能见到。
接下来我们看到char ph_name[_max]。其中_max的值为256。我相信你应该能理解这句话。第二个变量就是我们所看到的函数指针其中ph_p_func是函数指针其中函数指针指向的内容目前暂时还没有确定我们将在后续初始化这个结构变量的时候进行赋值。
我们来仔细看一下这两段宏的内容。
```
#define func_reg(fname) #fname, &amp;ph_##fname
#define func_lua(fname) int ph_##fname(lua_State* L)
```
其中func_reg是在给前面那个结构体初始化赋值的时候使用的因为我们知道如果我们需要给这个结构体赋值看起来的代码是这样
```
ph_func pobj = {&quot;test&quot;, &amp;test_func};
```
那么由于我们有大量的函数需要注册,所以我们将之拆分为宏,其中#fname的意思是将fname变为字符串而ph_##fname的意思是使用##字符,将前后内容连接起来。
通过这个宏比如我们输入一个a赋值给 fname那么#fname就变成字符串"a",通过 ph_##fname结果就是ph_a。
接下来的代码是方便在代码中编写一个一个lua注册函数用的所以很明显和上述的宏一样我们只需要输入a那么这个函数就变成了 int ph_a(lua_State* L)
定义好了这两个宏,我们怎么来应用呢?
```
func_lua(test_func);
ph_func p_funcs[] =
{
{ func_reg(test_func) },
};
func_lua(test_func)
{
const char *p1 = lua_tostring(L, 1);
const char *p2 = lua_tostring(L, 2);
// .... do something
lua_pushstring(L, &quot;something&quot;);
return 1;
}
void register_func(lua_State* L)
{
int i;
for(i=0; i&lt;sizeof(p_funcs)/sizeof(p_funcs[0]); i++)
lua_register(L, p_funcs[i].ph_name, p_funcs[i].ph_p_func);
}
```
首先联系上面的宏第一行代码是使用func_lua所以func_lua输入的宏参数是test_func。于是通过这个宏我们最终得到的函数名字是int ph_test_func(lua_State* L); 。
```
ph_func p_funcs[] =
{
{ func_reg(test_func) },
};
```
这段代码使用的是func_reg的宏。test_func最终在宏里面变成了 “test_func”以及&amp;ph_test_func函数指针。
最后我们来看一个重要的函数,**register_func**这个函数在后续将会输入一个Lua虚拟机指针但是我们要知道它在函数内部它做了什么东西。
```
int i;
for(i=0; i&lt;sizeof(p_funcs)/sizeof(p_funcs[0]); i++)
lua_register(L, p_funcs[i].ph_name, p_funcs[i].ph_p_func)
```
在循环里面我们计算p_funcs的结构数组的长度怎么计算的呢
首先我们使用sizeof编译器内置函数来取得p_funcs整个数组的长度这整个长度等于sizeof(ph_func)的值乘以数组长度。而ph_func结构体拥有一个字符串数组每个数组长度是256加上一个函数指针为4字节长所以长度是260。而如果有两个数组元素那就是520的长度。
以此类推,/sizeof(p_funcs[0]的意思是,我们取出第一个数组的长度作为被除数。事实上就是结构体本身的长度,所以就是结构体数组总长度除以结构体长度,就是一共有多少数组元素,随后进行循环。
在循环的过程中我们看到我们填入了结构体里面的两个变量ph_name以及ph_p_func这样一来我们只需要通过宏加上一些小技巧就可以把Lua的函数都注册到C程序里面我们假设这个C程序就是游戏的话那么我们很容易就可以和Lua进行互通了。
## 小结
我总结一下今天所讲的内容。
<li>
在Lua与C的结合过程中C语言需要新建一个Lua虚拟机然后使用虚拟机的指针来操作Lua函数。
</li>
<li>
在程序的应用中使用C语言中的一些宏的技巧可以使代码能够便利地应用在程序里。
</li>
最后,给你留一个小问题。
如果使用Lua往C语言传递一些内容比如从C语言获取Lua脚本中某个变量的值应该怎么做
欢迎留言说出你的看法。我在下一节的挑战中等你!

View File

@@ -0,0 +1,224 @@
<audio id="audio" title="第21讲 | 如何使用脚本语言编写周边工具?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/66/a9/6668f24a830dd8e9f9a604e0878f75a9.mp3"></audio>
上一节我们讲了脚本语言在游戏开发中的应用我列举了很多C语言代码这些代码做了这样一些事情
<li>
使用C语言和Lua语言进行沟通
</li>
<li>
在C语言代码里使用了宏和结构方便批量注册和导入C语言函数
</li>
<li>
Lua代码如何传输内容给C语言
</li>
<li>
Lua虚拟机堆栈的使用。
</li>
这一节我们要用Lua脚本来编写一个游戏周边工具Makefile。游戏周边工具有很多种并没有一个统一的说法比如在线更新工具、补丁打包工具、人物模型编辑工具、游戏环境设置工具等等。
你或许就会问了那我为什么选择Makefile工具来编写而不选择别的周边工具来编写呢
因为这个工具简单、小巧我们可以将Lua脚本语句直接拿来用作Makefile语句而在这个过程中我们同时还可以通过Lua语句来了解Lua的工作机理。 而且这个编写过程我们一篇文章差不多就可以说清楚。
而别的周边工具编写起来可能会比较复杂比如如果要编写类似Awk的工具的话就要编写文本解析和文件查找功能如果编写游戏更新工具的话就必须涉及网络基础以及压缩解压缩的功能。
简单直白地说Makefile是一种编译器的配置脚本文件。这个文件被GNU Make命令读取并且解析其中的意义调用C/C++(绝大部分时候)或者别的编译器(小部分)来将源代码编译成为执行文件或者动态、静态链接库。
我们可以自己定义一系列的规则然后通过顺利地运行gcc、cl 等命令来进行源代码编译。
我们先定义一系列函数来固定我们在Lua中所使用的函数。
```
int compiler(lua_State*);
int linker(lua_State*);
int target(lua_State*);
int source_code(lua_State*);
int source_object(lua_State*);
int shell_command(lua_State*);
int compile_param(lua_State*);
int link_param(lua_State*);
int make(lua_State*);
```
这些都是注册到Lua内部的C/C++函数。我们现在要将这些函数封装给Lua使用但是在这之前我们要将大部分的功能都在C/C++里编写好。
随后我们来看一下在Lua脚本里面具体是怎么实现Make命令操作的。
```
target(&quot;test.exe&quot;);
linker(&quot;c:\\develop\\dm\\bin\\dmc.exe&quot;);
compiler(&quot;c:\\develop\\dm\\bin\\dmc.exe&quot;);
source_code(&quot;c.cpp&quot;, &quot;fun.cpp&quot;, &quot;x.cpp&quot;);
source_object(&quot;c.obj&quot;, &quot;fun.obj&quot;, &quot;x.obj&quot;);
compile_param( &quot;$SRC&quot;, &quot;-c&quot;,
&quot;-Ic:/develop/dm/stlport/stlport&quot;,
&quot;c:/develop/dm/lib/stlp45dm_static.lib&quot;);
link_param(&quot;$TARGET&quot;, &quot;$OBJ&quot;);
make();
shell_command(&quot;del *.obj&quot;);
```
首先第一行对应的就是目标文件target函数后续的每一个Lua函数都能在最初的函数定义里找到。
在这个例子当中我们使用的是DigitalMars的C/C++编译器执行文件叫dmc.exe。我们可以看到在linker和compiler函数里都填写了dmc.exe说明编译器和链接器都是dmc.exe文件。
现在来看一下在C/C++里面是如何定义这个类的。
```
struct my_make
{
string target;
string compiler;
string linker;
vector&lt;string&gt; source_code;
vector&lt;string&gt; source_object;
vector&lt;string&gt; c_param;
vector&lt;string&gt; l_param;
};
```
为了便于理解我将C++类声明改成了struct也就是把成员变量改为公有变量你可以通过一个对象直接访问到。
随后我们来看一下如何将target、compiler和linker传入到C函数里面。
```
int compiler(lua_State* L)
{
string c = lua_tostring(L, 1);
get_my_make().compiler = c;
return 0;
}
int linker(lua_State* L)
{
string l = lua_tostring(L, 1);
get_my_make().linker = l;
return 0;
}
int target(lua_State* L)
{
string t = lua_tostring(L, 1);
get_my_make().target = t;
return 0;
}
```
在这三个函数里面我们看到get_my_make函数就是返回一个my_make类的对象。这个具体就不进行说明了因为返回对象有多种方式比如new一个对象并且return或者直接返回一个静态对象。
随后我们直接使用了Lua函数lua_tostring来得到Lua传入的参数比如如果是target的话我们就会得到”test.exe”并且将这个字符串传给my_make对象的 string target 变量。后续的compiler、linker也是一样的道理。
我们接着看下面两行。
```
source_code(&quot;c.cpp&quot;, &quot;fun.cpp&quot;, &quot;x.cpp&quot;);
source_object(&quot;c.obj&quot;, &quot;fun.obj&quot;, &quot;x.obj&quot;);
```
这两行填入了cpp源文件以及obj中间文件这些填入的参数并没有一个固定值可能是1个也可能是100个那在C/C++和Lua的结合里面我们应该怎么做呢
我们看到一个函数lua_gettop。这个函数是取得在当前函数中虚拟机中堆栈的大小所以返回的值就是堆栈的大小值比如我们传入3个参数那么返回的就是3。
接下来可以看到使用Lua的计数方式从1开始计数并且循环结束的条件是和堆栈大小一样大然后就在循环内将传入的参数字符串压入到C++的vector中。
随后的source_object、compile_param和link_param都是相同的方法将传入的参数压入到vector中。
你可能要问了我在Lua的代码中看到了$TARGET、$OBJ、$SRC等字样的字符串这些字符串的处理在哪里这些字符串又是做什么的呢
这些字符串是替代符号你可以理解为C语言中printf函数的格式化符号例如 “%d %s”等等虽然在这里这些符号都是自己定义的但是我们仍然需要解析它们。
其实解析的步骤并不难我们只需要将vector内的内容提取出来对比是不是字符串$TARGET等如果是的话就被替代为前面我们在target函数或者source_code函数中所定义的内容。
我们拿source_code部分来举例来看一下部分代码。
```
void run()
{
string command_line;
string src = &quot;$SRC&quot;;
string tar = &quot;$TARGET&quot;;
string obj = &quot;$OBJ&quot;;
for(int i = 0; i &lt; source_code.size(); i++)
{
..............
for(int j=0; j&lt;c_param.size(); j++)
{
if(c_param[j] == src)
{
command_line += source_code[i];
.....
}
}
}
```
在这部分的代码里面可以看到我们将压入的source_code内容进行循环。在循环之后必须对c_paramcompile_param也就是编译参数进行循环。当我们发现编译参数里面出现了$SRC这个替代字符串的时候就将source_code的内容其实就是源代码文件合并到command_line命令行里面去然后整合成为一个完整的、可以运行的命令行。
随后我再贴一部分代码,可以看到别的可替代字符串是怎么做的。
```
else if(c_param[j] == obj)
{
command_line += source_object[i];
}
else if(c_param[j] == tar)
{
command_line += target;
}
```
我们对替代字符串做了相同的比较如果是一致的话就将被替代内容添加到command_line变量里面组成一个完整的可运行命令行。
这个run函数其实就是在make的时候调用的函数。至于如何调用这一串command命令在C里面最简单的方式就是调用system函数或者使用execl函数系列。注意这个execl并不是来自微软的excel表格而是C语言的函数。
我们封装完了Lua部分的代码之后就需要将Lua的函数注册到Lua虚拟机里面这个我上一节已经具体说过了。
最后由于我们的Lua源代码本身就是一个Makefile文件所以我们不需要做过多的解析直接将这个源代码输入给Lua虚拟机即可。
```
string makefile;
ifstream in(&quot;my_makefile&quot;);
makefile = &quot;my_makefile&quot;;
if(!in.is_open())
{
in.close();
}
else luaL_dofile(L, makefile.c_str());
```
在这段代码里面我们首先使用C++的fstream库中的ifstream来尝试读取是不是有这个my_makefile文件如果没有的话就跳过并且关闭文件句柄如果存在的话就把这个文件填入到Lua虚拟机中让Lua虚拟机直接运行这个源文件。所以这种方式是最简单快捷的。
代码有点多,不要担心,我带你梳理一下今天的内容。
<li>
**利用C/C++语言和Lua源代码进行交互从Lua代码中获取数据并且在C语言里面进行算法的封装和计算最后将结果返回给Lua。** 我们在C/C++语言里面进行大量的封装和算法提取并且也利用C/C++进行调用和结果的呈现这是一种常用的方式也就是C语言占比60%70%Lua代码占比30%40%。
</li>
<li>
另一种比较好的方式是,**使用C/C++编写底层实现逻辑随后将数据传输给Lua让Lua来做逻辑运算最终将结果返回给C语言并且呈现出来**。这是很多人在游戏开发中都会做的事情比如我们编写地图编辑器先在Lua中编写好逻辑用C语言在界面中呈现出来即可。如果反过来做的话那就会出现大量的硬代码是很不合适的。所以这种情况下C语言占比30%40%Lua代码占比60%70%。
</li>
<li>
<p>**Lua可以是一种胶水语言。严谨地说像Python、Ruby等脚本语言都是合格的胶水语言。** 在这种情况下胶水语言起到的作用就是粘合系统语言C/C++)和上层脚本逻辑。所以,使用胶水语言,就像是一种动态的配置文件。<br>
按照普通的配置文件来讲你需要手工解析比如类似INI、XML、JSON等配置文件随后按照这些文件的内容来做出一系列的配置但是胶水语言不需要它本身就是一种动态的语言。<br>
你也可以把它当作一种配置的文件就像今天讲的Makefile它可以不需要你检测语法问题这些问题在Lua虚拟机本身就已经做掉了你需要做的就是将我们脑海里想让它做的事情通过C和Lua的库代码进行整合直接使用就可以了。所以**胶水语言的本身就是一个配置文件,同时它也是一个脚本语言源代码。**</p>
</li>
## 小结
在使用C/C++结合脚本语言的时候需要梳理这些内容比如哪些是放在C/C++硬代码里写的那些可以放到脚本语言里写梳理完后就可以将脚本语言和C/C++结合起来编写出易于修改脚本逻辑如果有不同需求可以很方便地改写脚本而不需要动C/C++硬代码)、易于使用的工具。
现在给你留一个小问题吧。
在Lua当中有table表的存在如何在C语言中给Lua源代码生成一个table表并且可以在Lua中正常使用呢
欢迎留言说出你的看法。我在下一节的挑战中等你!