lua

我的编程语言经历

Alan Perlis 说过:“一种不改变你编程的思维方式的语言,不值得去学。”,虽然写了这么多年程序,用了这么多的语言,但我自认还没悟道编程语言如何改变我的思维方式。

几天前,我需要用python来为ledisdb写一个客户端,我突然发现,对于c++,go这种语言,我如果需要实现一个功能,首先想到的是问题是代码应该怎么写。但是当我使用python的时候,我首先考虑的问题是在哪里去找一个库用来解决我的问题。可能这就是使用不同语言带给我的不同思考方式吧。

我的编程语言经历没有那么复杂,没用过很多,但是其实也够我受的了,尤其是在不同语言语法糖之间切换的时候,有种让人崩溃的感觉。没准我应该升级一下我的大脑cpu,使其能够更快速的进行中断处理。

c

我是从大学才开始学习编程的,相比现在的小朋友来说,可以叫做输在了起跑线上面。谁叫以前生活在山区,没机会接触电脑这玩意。

我的第一门编程语言是c,不同于很多童鞋使用的谭浩强神书,我用的是周纯杰的<>,不知道每年有多少同学受到过它的摧残,当然还有那哥们蹩脚的普通话。

在大学里面,很多同学的c的毕业设计都是我帮其完成,但我始终觉得自己仍然是个半吊子,除了c的基础稍微强一点之外,很多方面譬如操作系统,算法等完全不会。(现在随着工作年限的增加让我越发后悔,当初怎么就不稍微学点这些知识,尤其是编译原理。)

我几乎没怎么用c开发过项目,只在tencent可怜的维护过别人的c项目,但至少能看懂c代码,这就够了。

因为大多数时候,我用的是c++,而不是c来解决我的问题。

c++

c++是我工作使用的第一门语言,也是我使用时间最长的一门语言,都七年之痒了,不过还是有点不离不弃的。

以前上学的时候有一句口头禅,叫学好c++,走遍天下都不怕。但是有几个人能把它学好的?所以千万别说自己精通c++,那会被人鄙视的。

我使用c++可以分为三个阶段:

类c阶段

这个阶段主要是我第一份工作的时候,那时候才毕业,c的烙印很深,面向对象除了有个概念,真正是啥完全不知道。所以最喜欢的方式还是写着一堆函数来解决问题,当初VIA身边那帮c++的牛人竟然能忍受我这样的代码,真佩服他们。

面向对象阶段

后来去了第二家公司linekong,开始做游戏,才开始真正意义上的用c++写代码了。

记得最开始去第一家公司面试的时候,被问了啥是面向对象,当时不假思索的答了继承,多态和封装。

啥叫封装?整一个class,把该包的都包了,一个同事曾告诉我,他见过有几万行代码的class,看来我这个几千行的太小儿科了。

啥叫继承?先来一个父类,干脆叫bird,有一个fly方法,再来一个子类,叫duck吧,继承了bird,不过duck会fly吗?一个父类不够,再来一个,搞个多重继承,什么?出现了菱形继承,那干脆在来一个virtual继承得了。

啥叫多态?不就是virtual function,然后父类指针能在运行时根据实际情况调用相应的子类实现。那c++的多态是怎么实现的?看看<<深度探索c++对象模型>>不就行了。

这段时间,可以算是我写c++代码最多的时候,都快写到吐了,尤其还要忍受那龟速的编译。我们竟然都实现了直接通过汇编改c++的虚表,使其调用自己的函数这种变态的东西。在那时候我就得出结论,如果不想早死,尽量别用这个东西写代码。可是到如今我都在不停的慢性自杀。

现代C++阶段

不知道从什么时候开始,我突然觉得我应该来点modern c++的编写方式了,c++0x都出了,还不玩一下就晚了。当然新特性那么多,不可能全部都拿来用的,Bjarne Stroustrup貌似都说过,c++0x应该算是另一门语言了。

于是哥就走上了伪modern c++之路,class还是需要的,不然数据怎么封装。继承吗,比重减轻吧,最好采用面向接口的编程方式。而多态,能不用就不用吧,反而觉得bing + function的方式实现的delegate模型反而更方便,没准也更酷哟。

shared_ptr,weak_ptr是需要用的了,c++没有gc,所以一套好的内存管理方式是必不可少的,每次new之后还要记得delete也比较烦,最严重的是可能忘记那就内存泄露了。

于是,我就自认为我进化了,最典型的例子就是我写的高性能网络库libtnet,感觉很modern了。

lua

最开始知道lua,是云风那本编程感悟的书,当时可是菊花一紧,觉得这东西是啥,为什么能跟c结合到一起使用?

后来自己开发游戏了,才发现lua真的是一门很强大的语言,短小精悍,嵌入简单,性能超强,完全是作为游戏脚本语言开发的不二人选。不过我最开始使用lua的经历不怎么happy,最开始就是接手了一个c++与lua的粘合层库,关于这个库的传说,见这里Lua 不是 C++。后来,在踩了无数个坑,填了无数个坑之后,我终于弄得相对稳定了。貌似现在我以前的同事还在使用,不过正如我在lua c函数注册器中说明的那样,对于语言的交互,简单一点才好。现在以前做的游戏已经开源,见这里,那个传说中的蛋疼粘合层也见了世面。当然,我可不会告诉你们好多搓代码是我写的。

后来,在现在的公司,因为项目的需要,我们急需解决python的很多性能大坑问题,于是我开始推广使用openresty,一个用lua包裹的nginx,用了之后,腰不痛了,腿不痛了,性能妥妥的。

因为lua,我第一次尝到了在代码里面直接写配置的便捷,用一个table搞定,相比起来,c++处理ini,json这些的弱爆了。另外,动态语言的热更新机制使其代码升级异常方便,不过你得非常小心lua的闭包,没准你重新加载了代码运行还是老样子。

lua是一个动态语言,所以不用我们管内存释放问题,但是仍然可能会有引用泄露,在开发游戏的时候,为了解决我们程序lua内存泄露的问题,我曾经干过直接从_G递归遍历,扫描整个lua数据的事情。相比在c++使用valgrind这些程序的工具,lua配套的东西还是太小儿科了。

lua的调试也是一个大头问题,我曾今写过几个lua的调试器,例如这个,甚至都支持了类似gdb那样ctrl+c之后动态的设置断点,可是仍然没觉得比gdb方便,所以多数时候,我都是写log为主。

python

虽然小时候吃过很多蛇,但是蟒蛇可是从来没吃过的,现在看来python味道还不错。

我是来了kingsoft之后才开始正式使用python的。对于为啥使用python,我曾跟拉我进来的技术老大讨论过,他直接就说,开发快速,上手容易。

python开发的快速很大程度是建立在丰富的第三方库上面的,我们也使用了很多库,譬如tornado,gevent,django等,但是正如我最开始说的,因为我们有太多的选择,面对一个问题的时候,往往考虑的是如何选择一个库,而不是自己如何实现,这其实在某种程度上面使得很多童鞋知其然而不知其所以然。这点,lua可能是另一个极端,lua的定位在于嵌入式和高性能,所以自然地,你得自己动手造轮子(当然,现在也有很多好的第三方库了),虽然有时候写起来很不方便,但是至少自己很清楚程序怎么跑的。

当然,我绝对没有贬低python的意思,我很喜欢这门语言,用它来开发了很多东西,同时也知道很多公司使用python构建了很多大型稳定的系统(我们的产品应该也算吧)。

只是现在我越发觉得,看起来简单的语言,如果没有扎实的基本功底,写出来的东西也很烂,而python,恰恰给人放了一个很大的烟雾弹,你以为它有多容易,其实它是在玩你。

go

好了,终于开始说go了,let’s go!!!

我使用go的历史不长,可能也就一年多,但是它现在完全成了我最爱的语言,go具有了python开发的迅速,同时也有着c运行的性能。(当然,还是有差距的!)

网上有太多的语言之争,包括go,有人恨,有人爱。但萝卜白菜,各有所爱,对于我来说,能帮我解决问题,让我用着舒服的语言就是好语言。

go非常适用于服务端程序开发,比起用c++开发,我陡然觉得有一种很幸福的感觉,譬如对于网络编程,在c++里面,我需要自己写epoll的事件处理,而且这种异步的机制完全切分了整个逻辑,使得代码不怎么好写,我在开发libtnet的时候感触尤其深刻。但是在go里面,因为天生coroutine的支持,使得异步代码同步化了,非常利于代码的编写。

现在我的主要在项目中推动go的使用,我们已经用go搭建了一个高性能的推送服务器,后续还有几个系统会上线,而且开发的进度并不比使用python差,另外也很稳定,这让我对go的未来充满了期待。

我也用go写了很多的开源程序,也算是拿的出手了,譬如:

  • ledisdb:一个基于leveldb的提供类似redis高级数据结果的高性能NoSQL,真挺绕口的,简单点就是一个高性能NoSQL。
  • Mixer:一个mysql-proxy,现在支持通用的mysql命令代理,读写分离,以及自动主备切换。后续将要参考vitess支持分区,为此一直在恶补编译原理的知识。
  • go-log:一个类似python log模块的东西,支持多种handler,以及不同的log级别。

还有一些,可以参考我的github,譬如moonmq(一个高性能push模型的消息服务器),polaris(一个类似tornado的restful web框架),因为go,我开始热衷于开源了,并且认识了很多的好基友,这算得上一个很大的收获吧。

其它

好了,说完了上面我的长时间使用语言(至少一年以上),我也用了很多其他的语言,现在虽然使用时间比较短,但不排除后续会长久使用。

Objective-C

因为我家终于有了苹果三件套,所以决定开发app了,首要的任务就是得学会使用Objective-C。我承认这真是一门奇葩的语言,如果没有xcode的自动补齐,我真不知道写起来是神马样的感觉。

而且第一次见到了一门推荐写函数名,变量名就像写文章的语言,至少我写c++,go这些的一个函数名字不会写成一个句子。

我现在在自学iOS的开发,慢慢在整出点东西,毕竟答应给老婆的iphone做点啥的。后续干脆就写一个《小白学iOS》系列blog吧(如果我有精力!),反正对于iOS,我真是一个小白。

java

好吧,我也在android上面写过程序,build到我的S3上面去过,对于java,我的兴趣真不大,貌似自己还买过两本《java编程思想》,那时候脑袋铁定秀逗了。

但是不得不承认,java在服务器领域具有非常强的优势,很多很多的大企业采用java作为其服务器的编程语言。典型的就是淘宝,据传杭州的很多软件公司都不用java的,你用java就等于给淘宝培养人才了。(不过我发现他们很多基础组件譬如TFS这些的可是c++的哟!)

java是门好语言,只是我个人不怎么喜欢,可能我就是太小众了,只对c语言体系的感兴趣。所以很多公司我去不了,哈哈!

erlang

受《计算机程序的构造与解释》影像,我一直想学一门函数式编程语言,最开始玩的是elisp,谁叫以前我是个深度的emacser(后来竟然变成了一个vimer,再后来就是sublimer,这世界真神奇)。

后来还是决定好好学习一下erlang,也第一次领略到了函数式编程的魅力。自己唯一用erlang开发过的东西就是bt下载的客户端,不过后来发现用处不大就没继续进行下去了。(好吧,我承认当时想下岛国的东西)

学习erlang之后最大的优势在于现在能看懂很多优秀的erlang项目,譬如我在做moonmq以及公司的推送服务的时候,研究了rabbitmq,这玩意可是用erlang写的,而我竟然还能看懂,太佩服我了。

还有么?

想想自己还学了哪些语言?貌似没了,不知道awk算不算一门。看来我会得语言真不多。

后续可能会学的

逆水行舟,不进则退,计算机发展这么迅速,我也需要不断提升自己,其中学习一门新的语言可能是一个很好的提升途径,至少能为我打开一扇门。譬如,如果掌握了日文,就能更好的理解岛国片的精髓。我不会日文,所以还是个门外汉。

ruby

ruby是一门很优雅的语言,很多大神级别的人物推荐,github貌似也是ruby的幕后推手。

因为ROR的兴起,使得ruby更加流行。看来,一个好的框架库对于语言的推广帮助真挺大的。相比而言,python有django,tornado等,光选择适合自己的就得费点时间。

ruby可以算是一门完全面向对象的语言,连number这种的都是对象,而且看了几本Matz的书,觉得这哥们挺不错的,对技术的感悟很深,所以更让我有兴趣去了解ruby了。

javascript

作为一个技术人员,没一个自己的个人网站怎么行,我的阿里云都是包年买的(明年还是买国外的vps吧),自己的个人站点还无影无踪。

好吧,我完全不会javscript,看着css就头疼,没准我从一开始想自己写代码搭建个人站点这个步子就迈的太大,扯着蛋了。如果先用一个开源的框架搭建起来,再自己调整完善,可能会更好。但无论怎样,javascript这门语言还是要学习了解的,尤其是以后随着html5的流行,加之node.js疯狂流行,这门语言也会愈发的发光发热。

C Sharp

其实本来不准备跟ms的东西扯上关系的,虽然vs是一个很强大的开发工具,但是我自从换成mac之后就不准备再迁回windows。

只是c#我可能是必须要学会的,因为那个坑爹的unity3d,虽然unity3d也提供了其它语言的支持(譬如伪javascript),但是大量的开发者还是选用了c#,至少在中国我问过很多朋友,都妥妥的用c#,既然这样,我也只能考虑学习使用了。

至于我为啥蛋疼的想玩unity3d,毕竟干了很多年游戏开发,一直有自己弄一个简单小游戏的梦想,还是妥妥的unity3d吧。

自己造一个?

语言千千万,我不可能全部学会的,而且以后没准因为业务的需要,没准都会自己造一门语言,也就是DSL。不过这个貌似还离我比较遥远,编译原理的东西太差了(说多了都是泪呀)。自己写词法分析还成,后面就菜了。这也是Mixer一直没啥进展的原因。不过已经买了龙书,在学习屠龙秘籍,希望成为顶尖高手吧。

后记

写了这么多,看来随着年岁的增加,越来越啰嗦了。不是有一句古话:吾生也有涯,而知也无涯 。以有涯随无涯,殆已。不过不停地追逐不也是乐趣一件?

只是,现在我首先要做的就是向我老婆申请资金升级电脑了吧!

为什么使用Go

这里,我并不打算引起语言争论的口水仗,我并不是什么大牛,对语言的造诣也不深,只是想通过自己实际的经历,来说说为什么我在项目中选择go。

其他语言的经历

C++

在接触go之前,我已经有多年的c++开发经验。主要用在游戏服务端引擎开发以及P2P上面,那可是一段痛并快乐的时期,以至于我看到任何的程序钉子问题都觉得可以用c++这把锤子给敲定。但是对于互联网项目开发来说,除非你的团队整体的c++技术水平nb,并且有很强的代码规范,不然真可能是一场灾难,更别说我们现有团队几乎没其他人会这玩意了。

本来,我打算在现有项目中的推送系统中使用c++,并用业余时间写好了一个网络底层库libtnet,但后来还是决定打住,因为没有人能够协助我开发。令我比较欣慰的是,libtnet有一个游戏公司在使用,现处于内部测试阶段,即将放出去,我倒是很期待他们的好消息。

Lua

在做游戏的过程中,我也学会了lua这门语言,并且还有幸接触并完善了云风在Lua 不是 C++中提到的那个恐怖的lua,c++粘合层。

lua真的是一门非常好的语言,性能高,开发快速。不光游戏公司大量使用,在互联网领域,因为openresty的流行,一些公司(包括我们)也开始在web端使用lua进行开发。(颇为自豪的是还给openresty反馈过几个bug)

但是,lua因为太短小精悍,功能库并不多,很多需要自己去实现,而且,写出高性能,高质量lua代码也并不是很容易的事情。另外,因为其动态语言的特性,我们也栽了不少坑,这个后续在详说。

Python

在我来现有的团队之前,他们就已经使用python进行整个系统的开发,甚至包括客户端GUI(这对客户端童鞋当时就是一个灾难,后来换成Qt就舒爽了)。

python的好处不必说,从数不清的公司用它进行开发就知道,库非常丰富,代码简洁,开发迅速。

但是,在项目中经过两年多python开发之后,我们渐渐出现了很多问题:

  • 性能,python的性能是比较偏低的,对于很多性能热点代码,通常都会采用其他的方式实现。在我们的项目中,需要对任何API调用进行签名认证,认证服务我们开始使用的是tornado实现,但很不幸运的是,放到外网并没有顶住压力。所以我们引入openresty,专门用以解决性能问题。
  • 部署,python的库因为太丰富了,所以我们的童鞋引入了很多的库,个人感觉在pythoner的世界里面,可没有造轮子的兴趣。有时候发版本的时候,我们会因为忘记安装一个库导致程序无法运行。这可能跟我们团队没有成熟运维经验有关,后续通过salt,puppet这种发布工具应该能解决。
  • 质量,通常我们都认为,因为python代码的简洁,我们很容易的能写出高质量的代码,但是如果没有好的代码控制手段,用python也仍然能写出渣的代码,我甚至觉得因为其灵活性,可能会更容易写出烂的代码。这可以说是我们团队的教训。

这里,我并没有喷python的意思,它真的是一门好语言,我能够通过它快速的构建原型,验证我的想法,而且还一直在使用。只是在项目中,我们的一些疏忽,导致代码不可控了,到了不得不重构的地步了。

Why GO?

前面说了我的语言经历,以及项目到了重构地步的原因,但是为什么会是go呢?我们可以有很多其他的选择,譬如java,erlang,或者仍然采用python。我觉得有很多因素考量:

  • 静态,go是一门静态语言,有着强类型约束,所以我们不太可能出现在python中变量在运行时类型不匹配(譬如int + string)这样的runtime error。 在编译阶段就能够帮我们发现很多问题,不用等到运行时。(当然,这个静态语言都能做到)

  • 代码规范,很多人都比较反感go强制的编码规范,譬如花括号的位置。但我觉得,就因为强制约定,所以大家写出来的go代码样子都差不多,不用费心再去深究代码样式问题。而且我发现,因为规范统一,我很容易就能理解别人写的代码。

  • 库支持,go的库非常丰富,而且能通过go get非常方便的获取github,google code上面的第三方库(质量你自己得担着了),再不行,用go自己造轮子也是很方便的,而且造的轮子通常都比较稳定。

  • 开发迅速,不得不说,当你习惯用go开发之后,用go开发功能非常的快,相对于静态语言c++,开发的效率快的没话说,我觉得比python都不差,而且质量有保证。我们花了不到一个星期进行推送服务核心功能开发,到现在都没怎么变动,稳定运行。

  • 部署方便,因为是静态的,只需要build成一个可运行程序就可以了,部署的时候直接扔一个文件过去,不需要像python那样安装太多的依赖库。

GO特性

go现在的这个样子,有些人喜欢,有些人不喜欢,我无法知道为啥google那帮人把go设计成这样,但是我觉得,既然存在,就有道理,我只需要知道什么该用,什么不该用就可以了。

gc

GO提供了gc,这对于c++的童鞋来说,极大的减少了在内存上面犯错的机会,只是go的gc这个效率还真的不好恭维,比起java来说,还有很大的提升空间。

所以有时候写代码,我们还得根据tuning来提升gc的效率,譬如采用内存池的方式来管理大块的slice分配,采用no copy的方式来进行string,slice的互转。

不过go1.3貌似gc性能有了很大的改善,这点让我比较期待。

defer

go的defer其实是一个让人又爱又恨的东西,对于防止资源泄露,defer可是一个很不错的东西,但是滥用defer可是会让你面临很严重的内存问题,尤其是像下面的代码:

for {
    defer func(){
        //do somthing
    }
}

别以为go会在调用完成defer之后就好好的进行gc回收defer里面的东西,在我们进行内存profile的时候,发现大量的内存占用都是defer引起的。所以使用起来需要特别谨慎。

但我觉得,这个go应该会稍微改善,在go1.3里面,也有了对defer的优化。

error

也许error是一个让人争议很大的东西,现代方式的exception那里去呢?但是我觉得error能够非常明确的告诉使用者该函数会有错误返回,如果使用exception,除非文档足够详细,我还真不知道哪里就会蹦出一个异常了。

只是,go又提供了类似exception的defer,panic,recover,这是要闹哪出。

其实这篇文章我觉得已经解释的很好了,go程序的惯例是对外的API使用error,而内部错误处理可以用defer,recover和panic来简化流程。

其实这倒跟我一贯的编程准则对应,在团队在用python进行开发的时候,我们都明确要求库对外提供的API需要使用返回值来表示错误,而在内部可以使用try,catch异常机制。

interface

go提供了interface来进行抽象编程。何谓接口,最通常的例子就是鸭子的故事,“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子“。

在go里面,interface就是一堆方法的集合,如果某个对象实现了这些方法,那么该对象可以就算是该interface。使用interface,我们可以很方便的实现非侵入式编程,进行模块功能的替换。

对于长时间沉浸c++和python的童鞋来说,一下子要用interface来解决抽象问题,可能会很不适应。但当习惯之后,你会发现,其实interface非常的灵活方便。

写到后面

在使用的时候,我们需要知道go到底适用在什么地方,譬如我们现在也就将API服务使用go重构,我们可没傻到用go去替换openresty。

总之,go是一门很新的语言,国内也已经有很多公司开始吃这个螃蟹,也有成功的例子了,而我们也正开始了这段旅程。

Lua与C接口层开发

lua与c的交互

关于lua和c的交互,主要有两个方面,一是lua调用c的函数,而另一个则是c调用lua函数。而这些都是通过lua stack来进行的。

c调用lua

在c里面使用lua,主要是通过lua_call这类函数,下面来自lua manual的例子:

lua_getglobal(L, "f");                  /* function to be called */
lua_pushstring(L, "how");                        /* 1st argument */
lua_getglobal(L, "t");                    /* table to be indexed */
lua_getfield(L, -1, "x");        /* push result of t.x (2nd arg) */
lua_remove(L, -2);                  /* remove 't' from the stack */
lua_pushinteger(L, 14);                          /* 3rd argument */
lua_call(L, 3, 1);     /* call 'f' with 3 arguments and 1 result */
lua_setglobal(L, "a");                         /* set global 'a' */

该例子等同于直接在lua里面调用 a = f(“how”, t.x, 14)。
通过上面的例子可以看到,在c中使用lua是一件很容易的事情,首先获取需要调用的lua函数,然后将其需要的参数依次压入stack,然后通过lua_call调用,该函数调用的返回值也压入stack,供c去获取。

lua调用c

对于lua调用c,我们首先需要将c函数注册给lua,而注册给lua的函数,需要满足 int (*lua_CFunction)(lua_State* pState) 这种类型。如下例子:

int multi(lua_State* pState)
{
    int data1 = int(lua_tonumber(pState, 1));
    int data2 = int(lua_tonumber(pState, 2));
    lua_pushnumber(pState, data1 * data2);
    return 1;
}

lua_register(pState, multi, "multi");

#for use in lua
#a = multi(10, 20)

我们通过lua_register将mutli函数注册给lua,该函数接受两个参数,并且有一个返回值。当lua调用multi的时候,会将参数压入stack,所以我们可以通过lua_tonumber(pState, 1)和lua_tonumber(pState, 2)来获取,其中1为第一个参数10,2为第二个参数20。当运行完成之后,multi函数通过lua_pushnumber将结果压入lua堆栈,并通过return 1告知lua有一个返回值,为200。

可以看到,在lua中使用c也是一件很简单的事情。

lua mainly or c mainly

通过上面的例子可以看出,lua与c是很方便的交互的,但是在实际的游戏项目中,我们首先必须确定的一个问题就是,代码逻辑是以lua为主还是以c为主。

  • lua为主的游戏就是逻辑主要由lua负责,核心的对性能要求较高的逻辑则由c负责,游戏中的数据大多由lua负责。
  • c为主的游戏则是逻辑主要由c负责,lua只是负责简单的配置。

这两种方式都有优劣,对于实际游戏项目来说,个人认为,应该采用lua为主,c作为高性能核心的方式。之所以这样选择,是因为游戏的逻辑变动很大,我们需要快速的进行代码迭代,这个对于lua来说非常方便。而对于核心引擎,因为变化不大,同时对性能要求较高,所以采用c是一个很好的选择。

reg helper

在实际的游戏项目中,我们会遇到这样一个问题,假设c提供的函数为 int func(int a, int b),如果这个函数要提供给lua使用,我们需要写一个对应的注册函数,如下:

int func_wrapper(lua_State* pState)
{
    int a = int(lua_tonumber(pState, 1));
    int b = int(lua_tonumber(pState, 2));
    int ret = func(a, b);
    lua_pushnumber(pState, ret);
    return 1;
}

lua_register(pState, func_wrapper, "func");

对于任意的c函数,我们需要写一个对应的wrapper用来注册给lua。如果项目中只有几个c函数,那么无所谓,但是如果需要注册给lua的c函数很多,那么对于每一个c函数写一个wrapper,是一件很不现实的事情。并且如果c函数的参数或者返回值有变化,我们同时需要修改对应的wrapper函数。基于上述原因,我们需要一套自动机制,能够将任意的c函数注册给lua使用。实际来说,我们需要提供一个函数,对于任意的c函数func,我们只需要调用register(func, “func”),那么就能直接注册给lua使用。

traits

首先,我们必须面对的问题就是,不同的c函数,参数和返回值是不一样的,譬如对于int类型的参数,我们需要通过lua_tonumber获取数据,而对于const char* 类型的参数,我们需要通过lua_tostring来获取,同理对于返回值也一样。所以我们需要一套机制,根据c函数不同的参数和返回值类型来调用lua对应的stack操纵函数。我们可以通过c++ traits来实现。

我们提供如下一套函数:

template<typename T>
struct TypeHelper{};
bool getValue(TypeHelper<bool>, lua_State* pState, int index)
{
    return lua_toboolean(pState, index) == 1;
}
char getValue(TypeHelper<char>, lua_State* pState, int index)
{
    return static_cast<char>(lua_tonumber(pState, index));
}
int getValue(TypeHelper<int>, lua_State* pState, int index)
{
    return static_cast<int>(lua_tonumber(pState, index));
}
void pushValue(lua_State* pState, bool value)
{
    lua_pushboolean(pState, int(value));
}

void pushValue(lua_State* pState, char value)
{
    lua_pushnumber(pState, value);
}

void pushValue(lua_State* pState, int value)
{
    lua_pushnumber(pState, value);
}

通过traits技术,对于不同的参数类型,我们可以调用对应的getValue函数,来从lua获取实际的数据,而对于返回值,通过c++自动的参数匹配,就能调用对应的pushValue函数。

CCallHelper

上面解决了类型匹配的问题,下面就需要提供wrapper函数,用以封装c函数。这里我们提供call helper类来实现。

template<typename Ret>
class CCallHelper
{
public:
    static int call(Ret (*func)(), lua_State* pState)
    {
        Ret ret = (*func)();
        pushValue(pState, ret);
        return 1;
    }

    template<typename P1>
    static int call(Ret (*func)(P1), lua_State* pState)
    {
        P1 p1 = getValue(TypeHelper<P1>(), pState, 1);
        Ret ret = (*func)(p1);
        pushValue(pState, ret);
        return 1;
    }
};

CCallHelper提供了静态的call函数,第一个参数就是实际c函数,通过函数模板可以进行任意c函数的匹配,这里只提供了匹配无参数和一个参数类型的c函数模板,我们可以扩展到支持任意参数个数,但也别太多了。因为对于任意c函数来说,可能有一个返回值,也可能没有返回值,那么我们如何匹配没有返回值的c函数呢?这里就是为什么我们需要CCallHelper的原因。在c++中,是不支持函数级别的模板特化的,但是类却可以,所以我们通过特化CCallHelper来匹配无返回值的c函数。如下:

template<>
class CCallHelper<void>
{
public:
    static int call(void (*func)(), lua_State* pState)
    {
        (*func)();
        return 0;
    } 

    template<typename P1>
    static int call(void (*func)(P1), lua_State* pState)
    {
        P1 p1 = getValue(TypeHelper<P1>(), pState, 1);
        (*func)(p1);
        return 0;
    }
};

CCallDispatcher

通过CCallHelper::call(func, pState),我们就可以与lua进行交互,那么又如何调用到相应的CCallHelper呢?这里我们通过CCallDispatcher来进行,如下:

template<typename Func>
class CCallDispatcher
{
public:
    template<typename Ret>
    static int dispatch(Ret (*func)(), lua_State* pState)
    {
        return CCallHelper<Ret>::call(func, pState);
    }

    template<typename Ret, typename P1>
    static int dispatch(Ret (*func)(P1), lua_State* pState)
    {
        return CCallHelper<Ret>::call(func, pState);
    }
};

通过CCallDispatcher,我们就可以将不同的c函数dispatch到不同的CCallHelper上面。

CCallRegister and regFunction

解决了c函数派发调用的问题,最后我们就需要处理如何将任意的c函数注册给lua,代码如下:

template<typename Func>
class CCallRegister
{
public:
    static int call(lua_State* pState)
    {
        Func* func = static_cast<Func*>(lua_touserdata(pState, lua_upvalueindex(1));
        return CCallDispatcher<Func>::dispatch(*func, pState);
    }
};

template<typename Func>
void regFunction(lua_State* pState, Func func, const char* funcName)
{
    int funcSize = sizeof(Func);
    void* data = lua_newuserdata(pState, funcSize);
    memcpy(data, &func, funcSize);

    lua_pushcclosure(pState, CCallRegister<Func>::call, 1);
    lua_setglobal(pState, funcName);
}

首先,我们提供CCallRegister类,里面提供了一个static的call函数,该函数满足lua注册格式,所以实际我们是将该函数注册给lua,在call函数里面,我们通过lua_touserdata(pState, lua_upvalueindex(1))来获取实际的func,然后传递给CCallDispatcher进行派发。而将call注册则是通过regFunction,该函数将实际的c函数func存储在一个userdata中,然后将该userdata绑定到对应的CCallRgister call上面,作为一个upvalue,这样当在lua里面调用call函数的时候,通过lua_upvalueindex获取对应的upvalue,则可以取到实际的c函数。

一些设计上面的考虑

上述reghelper的实现,我已经放到github luahelper上面,并且在max os,gcc 4.2,lua5.2环境下面测试通过。

这里谈一些设计上面的问题,首先,我说的任意c函数,参数和返回值只能是基本数值类型,如bool,char,short,int,long,float,double以及字符串类型char*等,这里我并没有提供复杂类型譬如class,struct的支持。之所以这样考虑,是因为我想保证lua与
c交互的简单,云风曾经说过lua不是c++,我本人当年也曾经用了2年的时间做了同样的事情。但是实现了这套东西,功能是狠强大了,以至于可以把lua当成c++来用了,但是这真的是使用lua正确的方式吗?我现在觉得,引入复杂的结合层,反而在某些时候会带来更大的复杂性,导致语言侧重点的混淆。所以有时候,我反而觉得,对于这种语言的交互,可能使用其他方式,譬如json,反而来的更容易。

写在后面的话

对于lua和游戏开发的一些东西,其实一直想写,但是以前因为很多方面的原因而中途放弃。现在重新开始,有几个方面的原因,一个在于仍然对于游戏开发的热爱,做了4年的游戏开发,虽然现在从事云存储方面的研究,但是对游戏热情依旧。另一个方面在于lua5.2的发布,觉得是应该对以前游戏人生做一个总结了。

如果需要更强大的lua与c的交互,我觉得swig可能更合适。

Lua Debugger开发

introduction

工欲善其事,必先利其器。lua作为一门动态语言,虽然我已经习惯了使用print来进行代码调试,但是还是有很多童鞋觉得一款好用的调试器能更好的进行lua代码编写。所以在接手游戏的lua结合层之后,自然就需要提供一个debuger工具了。

我们只需要的是一个能快速进行lua代码调试的工具,所以不需要gdb那种额外复杂的功能,只需要提供几种简单的功能就行了,如下:

  • c/continue 继续执行
  • bt/backtrace 列出当前堆栈
  • f/frame n 跳转到frame n
  • l/list b e 列出源代码,b为起始行,e为结束行
  • p/print v 打印v的值
  • n/next 执行,跳过下一行,包括跳过子函数
  • s/step 执行,直到碰到不同的一行
  • return 执行,直到该函数结束

虽然调试器实现的功能很简单,但是对于大多数应用来说,已经完全足够使用。

lua debug library

lua提供了一个debug library,我们就通过这个库来实现一个调试器。
首先,我们需要注册一个lua debug hook,并且绑定LUA_MASKLINE,LUA_MASKRET,LUA_MASKCALL事件,这样当lua代码执行的时候如果碰到相应的事件,则会调用我们注册的debug hook。

当debug hook调用的时候,程序就进入debug模式,这时候就可以输入对应的命令进行执行。

db

只要注册了debug hook,那么每次lua代码执行的时候碰到对应的事件就会调用注册hook,如果每次调用都进入debug模式,那是很影响程序运行的,所以我们需要一种机制,只在需要的时候进入debug模式。

在gdb里面,我们通过设置断点来进入可调式模式,虽然在lua里面也可以这么做,但这里我们采用了一种更简单的方法。我们给lua注册一个db函数,当lua执行到db函数的时候,程序才会进入debug模式。因为lua是动态语言,如果我们需要在另一个地方也进行调试,只需要再次加入db函数,重启程序即可。

所以这里我们debug hook函数内部实现是一个状态机,当没有进入db的时候,虽然lua也会进行调用该hook,但该hook内部不作任何处理。只有当执行db函数进入debug模式,hook内部才会有相应处理。

我们在debug hook里面提供了多种状态,包括none hook,step hook,next hook和return hook。

  • none hook,没有进入debug模式,该hook不做任何处理
  • step hook,进入debug的step模式,当lua代码执行到新的一行代码时候做处理
  • next hook,进入debug的next模式,当lua代码跳过下一行时候做处理
  • return hook,进入debug的return模式,当lua执行到当前函数退出时候做处理

continue

continue会让程序继续执行。该命令会让hook切入none hook状态,直到下次lua执行db函数进入debug模式。

step

step会让hook切入step hook状态,该hook会监听LUA_MASKLINE事件,当该事件发生时候,step hook进行处理,打印当前代码,并再次进入debug模式,供下次命令输入。

next

next会让hook切入next hook状态,该hook也会监听LUA_MASKLINE事件,但是next跟step最大的区别在于next是跳过一行,也就是说如果执行的lua代码下一行是一个函数调用,step会进入函数内部,而next则会执行该函数,并跳过该函数这一行直到下一行。

所以next需要进行判断LUA_MASKLINE是否进入了一个新的函数,这里我们通过函数堆栈深度来进行,当lua代码执行到一个新的函数的时候,它的函数堆栈深度会加1,所以我们只需要记录当前的堆栈深度a,next执行到下一次LUA_MASKLINE时候,获取堆栈深度b,如果a小于b,那么表明进入了一个新的函数,所以我们不需要处理,直到再次获取的堆栈深度等于a。

return

return会让hook切入return hook状态,该hook会监听LUA_MASKRET事件,当该事件发生,return hook进行处理。这里我们仍然需要进入是否进入新函数调用的判断,因为我们只想监听的是当前函数的LUA_MASKRET事件,所以我们仍需要像next那样进行堆栈深度的判断。

backtrace/frame/list

backtrace,frame,list这几个命令这里列在一起,是因为他们都跟lua_getstack,lua_getinfo这两个函数有关系。我们通过lua_getstack初始化指定栈帧的lua_Debug结构,然后在通过调用lua_getinfo获取相关栈帧信息。

print value

print value命令可能算是最复杂的一个命令,因为有多个逻辑处理。当我们通过frame定位到某一层栈帧之后,就可以通过print打印相关的对象数据,供调试使用。

当print value的时候,首先我们查找value是否在当前函数里面local变量里面,如果没有则查找该函数的upvalue,如果仍然没有,则查找global,如果都没找到,则输出nil。

code

代码在luahelper的debughelper,只是一个简单的实现,还有一些问题需要考虑。

因为lua的debug hook注册的时候只能提供一个hook,所以为了简单起见,我对DebugHelper使用了单例模式,但是最好是一个lua实例对应一个debuger。要做到这样,自己想到了两种可能方法:

  • 使用LUAI_EXTRASPACE,并通过luai_userstateopen,luai_userstateclose将debuger绑定到lua实例上面,并通过luai_userstatethread进行debuger在coroutine的迁移。这种方法需要重新编译lua代码,适合集成lua源码的项目。
  • debuger内部使用一个map来进行对应,但是在lua里面需要替换coroutine的创建,因为创建的coroutine也需要对应到同一个debuger上面。

两种方法都懒得弄了,以后有机会去尝试一下。

end

这里只是简单的实现了一个lua debuger,但是功能我觉得足以可以在实际项目中应用了。只是越来觉得,对于动态语言,print和log才是我最喜欢的代码调试方式,因为简单而且强迫你去思考整个程序的运行流程。不过把debuger放在这里,也算是对自己以前游戏开发的一个总结吧。