云风的BLOG:语言与设计Archives

我在用C构建项目,尤其是和Lua混合使用时,一直很头疼C没有一个统一的模块管理器。Lua的模块管理虽然简单,但毕竟有且够用。一种方法是把C模块封装成一个个Lua模块,让Lua帮助管理,每个C模块是独立的,相互不可见。

但当C模块之间发生关系时,就比较麻烦。当然,简单的方法是通过链接器把它们都链接在一起,通过函数名前缀以区分。或是利用操作系统的动态库加载器来管理模块。

最近有了一点有趣的想法,觉得一个最简的模块管理器其实复杂度并不高。花了半天功夫实现了一下,感觉还不错。

我的观点是:如果一个系统的某个模块有可能使用10G这个量级的内存,那么它必然是一个核心问题需要专门对待。核心问题应该有核心问题的考量方法,这不是GC之错,也并非手动管理内存一条解决之道。即使手工管理内存,也无非是把内存块之管理转嫁到一个你平常不太想关心的“堆”这个数据结构上,期待有人实现了一个通用方案尽可能的帮你解决好。如果随意使用,一样有类似内存碎片无法合并之类的问题,吃掉你额外的内存。如果它存在于你的核心模块,你一样需要谨慎考量。

VLA(可变长度数组)是C语言在C99之后加入的一个很方便的语言特性,但是MSVC已经明确不支持VLA了。而且Linux的内核代码中曾经使用过VLA,而现在已经移除了VLA。看起来,VLA带来的安全问题比它的便利性要多。

但是,日常用C语言做开发时,经常还是需要变长数组的。既然直接用C语言的VLA有诸多问题,那么还是需要额外实现一个比较好。C没有C++那样的模板支持,一般的通用VLA实现很难做到类型安全。即使用C++,STL中的vector,这个最常用的VLA实现,也不总是切合应用场景的。比如,std::vector它的数据一般还是分配在堆上,而不是栈上。相比原生创建在栈上的数组,它可能性能有影响且有可能制造更多的堆内存碎片。

我认为一个通用的VLA库,应该做到:

这套脚本语言还是以配置数据为主,但也提供了很多逻辑控制手段,很值得学习。

TL;DR在花了一整个晚上用C++完成了这一块的功能后,我陷入了自我怀疑中。到底花这么多精力做这么一小块功能有意义么?强调类型安全无非是为了减少与之关联的代码的缺陷,提高质量;但代码不那么浅显易懂却降低了质量。

先来回顾一下设计:在这个粒子系统中,我期望把粒子对象的不同属性分开管理。

这是因为,在处理单个属性时,往往并不关心别的属性。比如,我们在递减生命期,处理生命期结束的对象时,关心的仅仅是生命期这个属性;在处理粒子受到的重力或其它力的影响时,我们只关心当前的加速度和速度;在计算粒子的空间位置时,只关心上一次的位置和瞬间速度;而在渲染时候,无论是生命期、加速度、速度,这些均不关心。

当数据按属性聚合,代码在批量处理数据时,连续内存对cache友好,即使属性只有一个字节,也不会因为对齐问题浪费内存。同一属性的数据尺寸完全相同,处理起来更简单。而且粒子对象相互不受影响,我们只是把同一个操作作用在很多组数据上,次序不敏感。非常适合并行处理。

更重要的是,不同类型的粒子需要自由的根据需要组合属性和行为。有的粒子有物理信息参与刚体碰撞运算,有的则只需要显示不需要这个信息;有的粒子有颜色信息,有的不需要有;有的粒子是一个面片,有的却是一个模型,拥有不同的材质。这导致粒子对象包含的信息量是不同的。及时拥有同一属性,作用在上面的行为也可能不同:例如同样是物理形状信息,可能用于刚体碰撞,改变运动轨迹,也可能只是为了触发一下碰撞事件。

在传统的面向对象的方式中,常用多态(C++的虚函数)来实现,或者有大量的ifelseswitchcase。

如果能按组件和行为聚合,那么就能减少大量的分支。每个粒子的功能组合(打开某个特性关闭某个特性)也方便在运行时决定,而不用生成大量的静态类。

这几天在重构引擎中的粒子系统。之前用lua做了个原型,这次用C/C++重新实现一次。目前还是基于CPU的粒子系统,今后有必要再实现基于GPU的版本。

首先,粒子对象本身就是一个集合了多种数据的数据块。我限制了同时最多64K个粒子片,这些粒子对象可以放在一块连续内存中,并且可以用16bit的id进行索引。

对于3d库来说,API涉及大量的内存块的操作。创建Buffer,贴图,shader,都需要输入一个数据块。大多数数据块是只读的,少部分是需要回写的。对于只读数据块,封装层可以用luastring替代,可写的用userdata。

bgfx自己抽象了一个叫做Memory的结构,用来统一描述这类内存块对象。按bgfx的定义,Memory的构造由用户决定,而释放通常由bgfx管理,而非调用者。

即,用户负责构造出Memory对象,将数据拷贝进去,然后再传递给bgfx的api后就可以撒手不管了。但是,如果你构造出Memory对象不传递给bgfx则会造成内存泄漏(因为没有任何直接释放它的方法);也不可以将一个Memory对象使用多次(传递给bgfx多次),因为一旦传给bgfx,就失去了对象的控制权。

我们上周在游戏引擎上面的工作中遇到一些bug,涉及到过去的一些设计问题。维持讨论了几天解决该问题的方案。今天终于把最终方案确定了下来,值得做一个记录。

bug出在游戏资源文件的转换上面。

游戏里用到的资源通常需要一个导入资源库的过程,例如你的原始贴图是一个png文件,但是引擎需要的是对应运行平台的压缩格式,windows上是dxt,手机上是ktx等等。这个过程,在Unity等商业引擎中,是放在资源导入流程中。

我更希望转换过程是惰性的,直到最终运行需要的资源才需要转换。

我在2006年左右尝试利用COM的IDL做过一些工作,想来这和COM面临的问题是一样的。我认为lua是一个可以用来做DSL/IDL非常好的工具,所以我向bgfx项目推荐了它。而且bgfx用的构建工具Genie就是基于lua的,这样工具链就比较统一。

它和很多为Lua封装的数学运算模块不同,并没有用userdata或table来实现矩阵向量对象,而是采用了64bit的整数id。其生命期管理是模拟了一个堆栈,我称之为数学栈,和程序运行时存放临时变量的堆栈以示区分。数学栈上存在过的矩阵向量对象的生命期都会维持一个固定周期,通常是一个渲染帧。每帧主动调用重置指令刷新。这个设计减少了在lua层面使用矩阵向量对象的心智负担。你不必担心运算产生的临时结果会增加过多gc的负担。构造新的矩阵向量对象的成本也非常的小。

最近两个月,结合过去的经验,我们对最初设计的框架做了较大的调整。这主要是源于对框架要解决的事情的更深入的理解,以及在实践过程中针对典型场景总结出来的模式。

在此之前,我们一直在直接使用lua描述数据;但最近随着数据类型系统的完善,同事建议设计一种专有数据格式会更好。希望专用格式手写和阅读起来能比lua方便,对diff更友好,还能更贴近我们的类型系统,同时解析也能更高效一些。lua的解析器虽然已经效率很高,但是在描述复杂数据结构时,它其实是先生成的构造数据结构的字节码,然后再通常虚拟机运行字节码才构造出最终的数据结构。这样的两步工作会比一趟扫描解析构造要慢一些且消耗更多的内存。

现有的流行数据格式都有一些我们不太喜欢的缺点:

所以ECS框架改变了数据组织方式,把同类数据聚合在一起,并用专门的业务处理流程只针对特定数据进行处理。这就是C和S的概念:Component就是对象的一个方面aspect的数据集,而System就是针对特定一个或几个aspect处理方法。

那么,Entity是什么呢?

我认为Entity主要解决了三个问题。

随后交给开发组的一个同学实现,这半年来,一直在使用。最近做了引擎一个小版本的内部验收,我感觉这块东西还有比较大的改进余地。因为资源文件系统目前和开发期资源在线更新部分现在掺杂在一起,而网络更新部分似乎还有些bug,偶尔会卡住。我觉得定位bug成本较高,不如把这块重新实现一遍,顺便把新的改进想法加进去。

如果用纯lua来做向量/矩阵运算在性能要求很高的场合通常是不可接受的。但即使封装成C库,传统的方法也比较重。若把每个vector都封装为userdata,有效载荷很低。一个floatvector4,本身只有16字节,而userdata本身需要额外40字节来维护;4阶float矩阵也不过64字节。更不用说在向量运算过程中大量产生的临时对象所带来的gc负担了。

采用lightuserdata在内存额外开销方面会好一点点,但是生命期管理又会成为及其烦心的事。不像C中可以使用栈作临时储存,C++中有RAII。且使用api的时候也会变得比较繁琐。

最近在用Lua实现一个ECS框架,用到了一些有趣的Lua语法技巧。

在ECS框架中,Component是没有方法只有数据的,方法全部写在System中。Entity本身仅仅是Component的组合,通常用一个id表示。

但实际写代码的时候,使用面向对象的语法(用Lua的冒号这个语法糖)却是比较自然的写法。比如我们在操作一个Component数据的时候,用component:foobar()比用foobar(component)要舒服一些。好在Lua是一门非常动态的语言,我们有一些语法技巧在保持上面ECS原则的前提下,兼顾编码的书写体验。

最近在基于ECS模型做一些基础工作。实际操作时有一个问题不太明白,那就是涉及对象(entity)集合本身的System到底应该怎样处理才合适。

仔细阅读了能找到的关于ECS的资料,网上能找到的大多是几年前甚至10年前的。关于ECS的资料都不断地强调一些基本原则:C里面不可以有方法(纯数据结构),S里面不可以有状态(纯函数)。从这个角度看,Unity其实只是一个EC系统,而不是ECS系统。从Unity中寻找关于System的设计模式恐怕并不合适。

重看了一遍暴雪在今年GDC上的演讲OverwatchGameplayArchitectureandNetcode——这可能是最新公开的采用ECS模式的成功(守望先锋)实践了——我想我碰到的基础问题应该在里面都有答案。

我的这个观点也不新鲜,在ECS的Wikipedia页上也有类似的说法:

IntheoriginaltalkatGDCScottBilascomparesC++objectsystemandhisnewCustomcomponentsystem.ThisisconsistentwithatraditionaluseofthistermingeneralSystemsengineeringwithCommonLispObjectSystemandTypesystemasexamples.Therefore,theideasof"Systems"asafirst-classelementisapersonalopinionessay.Overall,ECSisamixedpersonalreflectionoforthogonalwell-establishedideasingeneralComputerscienceandProgramminglanguagetheory.Forexample,componentscanbeseenasamixinidiominvariousprogramminglanguages.Alternatively,componentsarejustasmallcaseunderthegeneralDelegation(object-orientedprogramming)approachandMeta-objectprotocol.I.e.anycompletecomponentobjectsystemcanbeexpressedwithtemplatesandempathymodelwithinTheOrlandoTreatyvisionofObject-orientedprogramming,

EntityComponentSystem(ECS)是一个gameplay层面的框架,它是建立在渲染引擎、物理引擎之上的,主要解决的问题是如何建立一个模型来处理游戏对象(GameObject)的更新操作。

传统的很多游戏引擎是基于面向对象来设计的,游戏中的东西都是对象,每个对象有一个叫做Update的方法,框架遍历所有的对象,依次调用其Update方法。有些引擎甚至定义了多种Update方法,在同一帧的不同时机去调用。

我觉得守望先锋之所以要设计一个新的框架来解决这个问题,是因为他们面对的问题复杂度可能到了一个更高的程度:比如如何用预测技术做更准确的网络同步。网络同步只关心很少的对象属性,没必要在设计同步模块时牵扯过多不必要的东西。为了准确,需要让客户端和服务器跑同一套代码,而服务器并不需要做显示,所以要比较容易的去掉显示系统;客户端和服务器也不完全是同样的逻辑,需要共享一部分系统,而在另一部分上根据分别实现……

我在自己的机器上暂时无法重现问题,从分析上看,这个制造问题的fd是0,也就是stdin,猜想和重定向有关系。

skynet当初并没有处理EPOLLERR的情况(在kqueue中似乎没有对应的东西),这个我今天的patch补上了,不过应该并不能彻底解决问题。

我做了个简单的测试,如果强行closefd0,而在close前不把fd0从epoll中移除,的确会造成一个不再存在的fd(0)不断地制造EPOLLIN消息(和issue中提到的不同,不是EPOLLERR)。而且我也再也没有机会修复它。因为fd0被关闭,所以无法在出现这种情况后从epoll移除,也无法读它(内核中的那个文件对象),消息也就不能停止。

在很多年前,我在我经手的一些项目中使用googleprotocolbuffers。用了好几年,经历了几个项目后,我感觉到它其实是为静态编译型语言设计的协议,其实并没有脱离语言的普适性。在动态语言中,大家都不太愿意使用它(json更为流行)。一个很大的原因是,protobuffers是基于代码生成工作的,如果你不使用代码生成,那么它自身的bootstrap就非常难实现。

这两个月,我的主要工作是跟进公司内一个MMORPG项目,做一些代码审查提出改进意见的工作。

在数月前,项目经理反应程序不太稳定,经常出一些错误,虽然马上就可以改好,但是随着开发工作推进,不断有新的bug产生。我在浏览了客户端的代码后,希望修改一下客户端的UI框架以及消息分发机制等,期望可以减少以后的bug出生概率。由于开发工作不可能停下来重构,所以这相当于给飞行中的飞机换引擎,做起来需要非常小心,逐步迭代。

工作做了不少,其中一个小东西我觉得值得拿出来写写。

我希望UI部分可以严格遵守MVC模式来实现。其实道理都明白,但实际操作的时候,大部分人又会把这块东西实现得不伦不类。撇开各种条条框框,纸上谈兵的各种模式,例如MVCMVPMVVM这些玩意,我认为核心问题不在于M和V大家分不清楚,而是M和V产生联系的时候,到底应该怎么办。联系它们的是C还是P或是VM都只为解决一个问题:把M和V解耦。

昨天在review我公司一个正在开发的项目客户端代码时,发现了一些坏味道。

客户端框架创建了一个简单的对象系统,用来组织客户端用到的对象。这些对象通常是有层级关系的,顶层对象放在一个全局集里,方便遍历。通常,每帧需要更新这些对象,处理事件等等。

顶层每个对象下,还拥有一些不同类别的子对象,最终成为一个森林结构,森林里每个根对象都是一颗树。对象间有时有一些引用关系,比如,一个对象可以跟随另一个对象移动,这个跟随就不是拥有关系。

这种设计方法或模式,是非常常见的。但是在实现手法上,我闻到了一丝坏味道。

今天在公司群里,Netbug同学提出了一个问题,围绕这个问题大家展开了一系列讨论。讨论中谈及了lua中的一个常见的模式:propertytable,我觉得挺有意思,记录一下。

最初的问题是:当一个对象的某些属性并不常用,希望做惰性初始化的话,应该怎么实现。

我认为,propertytable是一个很符合这个案例的常见模式。

比如,对象f有三个可能的成员abc,我们可以不把f.af.bf.c记录在f这个table里,而是额外有三张大表,abc。利用metatable,可以在访问f.a的时候,实际访问的是a[f]。也就是说,所有同类对象的a属性,都是从a这张表里访问的。

a这张表的key就是对象,value是对象对应的a属性值。

无论是客户端还是服务器,把lua作为嵌入语言使用的时候,都在某种程度上希望把lua脚本做多线程使用。也就是你的业务逻辑很可能有多条业务线索,而你希望把它们跑在同一个luavm里。

lua的coroutine可以很好的模拟出线程。事实上,lua自己也把coroutine对象叫做thread类型。

最近我在反思skynet的lua封装时,想到我们的主线程是不可以调用阻塞api的限制。即在主干代码中,不可以直接yield。我认为可以换一种更好(而且可能更简洁)的封装形式来绕过这个限制,且能简化许多其它部分的代码。

下面介绍一下我的新想法,它不仅可以用于skynet也应该能推广到一切lua的嵌入式应用(由你自己来编写host代码的应用,比如客户端应用):

最近在尝试重新写skynet2.0时,把过去偶尔用到的一个对象生命期管理的手法归纳成一个固定模式。

其中,对象在获取其引用传入处理函数中处理时,将对象的引用加一,处理完毕再减一。这就是常见的基于引用计数的对象生命期管理。

常规的做法(包括C++的智能指针)是这样的:对象创建时,引用为1(或0)。每次要传给另一个处地方处理,或保留待以后处理时,就将其引用增加;不再使用时,引用递减。当引用减为0(或负数)时,把对象引用的资源回收。

由于此时对象不再被任何东西引用,这个回收销毁过程就可视为安全且及时的。不支持GC的语言及用这些语言做出来的框架都用这个方式来管理对象。

这个手法的问题在于,对象的销毁时机不可控。尤其在并发环境下,很容易引发问题。问题很多情况是从性能角度考虑的优化造成的。

加减引用本身是个很小的开销,但所有的引用传递都去加减引用的话,再小的开销也会被累积。这就是为什么大多数支持GC的语言采用的是标记扫描的GC算法,而不是每次在对象引用传递时都加减引用。

大部分情况下,你能清楚的分辨那些情况需要做引用增减,哪些情况下是不必的。在不需要做引用增减的地方去掉智能指针直接用原始指针就是常见的优化。真正需要的地方都发生在模块边界上,模块内部则不需要做这个处理。但是在C/C++中,你却很难严格界定哪些是边界。只要你不在每个地方都严格的做引用增减,错误就很难杜绝。

使用id来取代智能指针的意义在于,对于需要长期持有的对象引用,都用id从一个全局hash表中索引,避免了人为的错误。(相当于强制从索引到真正对象持有的转换)

id到对象指针的转换可以无效,而每次转换都意味着对象的直接使用者强制做一个额外的检查。传递id是不需要做检查的,也没有增减引用的开销。这样,一个对象被多次引用的情况就只出现在对象同时出现在多个处理流程中,这在并发环境下非常常见。这也是引用计数发挥作用的领域。

而把对象放在一个集合中这种场景,就不再放智能指针了。

长话短说,这个流程是这样的:

将同类对象放在一张hash表中,用id去索引它们。

所有需要持有对象的位置都持有id而不是对象本身。

需要真正操作持有对象的地方,从hash表中用id索引到真正的对象指针,同时将指针加一,避免对象被销毁,使用完毕后,再将对象引用减一。

前一个步骤有可能再id索引对象指针时失败,这是因为对象已经被明确销毁导致的。操作者必须考虑这种情况并做出相应处理。

看,这里销毁对象的行为是明确的。设计系统的人总能明确知道,我要销毁这个对象了。而不是,如果有人还在使用这个对象,我就不要销毁它。在销毁对象时,同时有人正在使用对象的情况不是没有,并发环境下也几乎不能避免。(无法在销毁那一刻通知所有正在操作对象的使用者,操作本身多半也是不可打断的)但这种情况通常都是短暂的,因为长期引用一个对象都一定是用id。

了解了现实后,“当对象的引用为零时就销毁它”这个机制是不是有点怪怪的了?

明明是:我认为这个对象已经不需要了,应该即使销毁,但销毁不应该破坏当下正在使用它的业务流程。

这次,我使用了另一个稍微有些不同的模式。

每个对象除了在全局hash表中保留一个引用计数外,还附加了一个销毁标记。这个标记只在要销毁时设置一次,且不可翻转回来。

现在的流程就变成了,想销毁对象时,设置hash表中关联的销毁标记。之后,检查引用计数。只有当引用计数为0时,再启动销毁流程。

任何人想使用一个对象,都需要通过hash表从id索引到对象指针,同时增加引用计数,使用完毕后减少引用。

另外,对象的创建和销毁都是低频率操作。尤其是销毁时机在资源充裕的环境下并不那么重要。所以,所有的对象创建和销毁都在同一线程中完成,看起来就是一个合理的约束了。尤其在actor模式下,actor对象的管理天生就应该这么干。

有了单线程创建销毁对象这个约束,好多实现都可以大大简化。

那个维护对象id到指针的全局hash表就可以用一个简单的读写锁来实现了。索引操作即对hash表的查询操作可遇见是最常见的,加读锁即可。创建及销毁对象时的增删元素才需要对hash表上写锁。而因为增删元素是在同一线程中完成的,写锁完全不会并发,对系统来说是非常友好的。

对于只有唯一一个写入者的情况,还存在一个小技巧:可以在增删元素前,复制一份hash表,在副本上慢慢做处理。只在最后一个步骤才用写锁把新副本交换过来。由于写操作不会并发,实现起来非常容易。

起因是最近有人在skynet邮件列表里贴了段错误log,从log显示,他在table.sort的比较函数里调用了skynet的snaxrpc去获取远程数据。然后被lua无情的报了attempttoyieldacrossaC-callboundary。

通过这件事,我反而觉得none-yieldable的限制反而提前阻止了一个错误的实现,其实是应该庆幸的。

在这一篇blog中,不想讨论protobuf的优劣,只谈谈sproto中如何使用rpc的api。这是sproto的api文档中没有写明,而很多想用它的同学问起的问题。

问题是这样的:

sproto支持数组,但很多情况下,业务处理中,我们并不用数组来保存大量的相同类型的结构数据。因为那样不方便检索。

比如你要配置若干地图表、NPC表等等的信息,固然可以用sproto的array来保存。但是在运行时,你更希望用定义好的id来检索它们。如果sproto不支持unorderedmap的话,你就需要在decode之后,对arraytable做一次遍历,用一张新表来建立索引。

googleprotocalbuffers2也有这个问题,据说第3版要增加map用来兼容json,这个话题最后再说。

有更多项目的参与的情况下,原来ejoy2d的简单构架慢慢显出一些局限性。主要是不同的项目会根据项目的需要(通常是针对某些特定需求的优化,以及特别的效果需求)修改底层shader的部分。最早设计的时候,因为考虑到只是用于2d游戏的开发,所以把shader模块实现的比较简单。特别是attributelayout是固定的,而uniform管理也没有留下太多扩展性。

在现代手机的GPU架构下,从渲染层渲染层API看,其实2d和3d其实没有本质上的区别。都是基于三角片渲染的。需要把顶点上传到GPU中由vs处理,在最后对像素做fs渲染出来。

而2dengine和3dengine的区别通常在于2dengine的顶点变换很简单。不需要用projectionmatrix和viewmatrix做变换。2dengine中的对象多半是四边形,数量很多,常见的优化手法是将大量的四边型合并到同一个渲染批次中;所以worldmatrix(以平移变换为主)在CPU中和顶点计算再提交更常见一些。

2dengine从应用上说,就是在处理一张张图片。所以对图片(四边型)的处理的变化要多一些。这使得fs要多变一点,需要引擎提供一定的可定制性。但很少去处理3dengine常见的光照投影这些东西。更多的是为了优化贴图用量等目的而技巧性的去使用一些图片。

突出2dengine的专门面对的业务的特性,而简化GPU提供的模型,用简短的代码构建engine框架,是ejoy2d设计的初衷。而且我也相信,简单可以带来更好的性能。所以一开始设计ejoy2d的时候,shader模块的很多东西都被写死了,以最简单的方式达到目的。仅暴露了很少的外部接口,再在这些有限的接口上设计数据结构,做性能优化。

对于不常更新的数据,我在skynet里增加了sharedata模块,用于配置数据的共享。每次更新数据,就将全部数据打包成一个只读的树结构,允许多个luavm共享读。改写的时候,重新生成一份,并将老数据设置脏标记,指示读取者去获取新版本。

我希望再设计一套方案解决这个实时性问题,可以用于频繁的数据交换。(注:在mmorpg中,很可能被用于同一地图上的多个对象间的数据交换)

一开始的想法是做一个支持事务的树结构。对于写方,每次对树的修改都同时修改本地的luatable以及被修改patch累计到一个尽量紧凑的序列化串中。一个事务结束时,调用commit将快速mergepatch。并将整个序列化串共享出去。相当于快速做一个快照。

读取者,每次读取时则对最新版的快照增加一次引用,并要需反序列化它的一部分,变成本地的luatable。

我花了一整天实现这个想法,在写了几百行代码后,意识到设计过于复杂了。因为,对于最终在lua中操作的数据,实现一个复杂的数据结构,并提供复杂的C接口去操作它性能上不会太划算。更好的方法是把数据分成小片断(树的一个分支),按需通过序列化和反序列化做数据交换。

在实现过程中,发现了许多编码格式上可以优化的地方,所以一边实现一边做调整,使结构更适合编码和解码,并且更紧凑。

做了如下改动:

由于这个东西主要binding到lua这样的动态语言中使用,所以我不需要按Cap'nProto那样,直接访问编码后的数据结构(直接把数据结构映射为C/C++对象),所以数据对齐是不必要的。

编码时的tag如果要求严格升序也可以更快的处理数据,减少实现的复杂度。数据段也要求按持续排列,且不准复用。这样可以让数据中有更多的0方便压缩。

把boolean数组按位打包的意义也不太大(会增加实现的复杂度)。

暂时先不实现64bitid的类型。(以后再加)

最终的WireProtocol是这样的:

经过近三年的使用,我发现其实我们用不着那么复杂的协议,里面很多东西都可以简化。而另一方面,我们总需要再其上再封装一层RPC协议。当我们做这层RPC协议封装的时候,这个封装层的复杂度足以比全新设计一套更合乎我们使用的全部协议更复杂了。

由于我们几乎一直在lua下使用它,所以可以按需定制,但也不局限于lua使用。这两天,我便构思了下面的东西:

为什么大部分网络服务都需要一个数据库在后台支撑整个系统?

当数据量巨大时,任何对数据的操作的算法和数据结构都需要精心设计,这不是随便一个程序员就可以轻松完成的任务。尤其是数据量大到超过内存容量时,很多算法和数据结构对大部分非此领域的程序员来说都是陌生的。本着专业的事情交给专业的人来做的原则,一般系统都会把这部分工作交给独立的数据库来完成。

对数据的操作只有抽象的足够简单,系统才能健壮,这便有了SQL语言做一层抽象,让数据管理的工作可以独立出来。甚至于你想牺牲一部分的特性来提高性能,还可以选用近年来流行的各种NOSQL数据库。

可在MMO游戏服务器领域,事情发生了一点点变化。

最大的矛盾是:MMO游戏中数据集的改变不再是简单的SQL可以表达的东西,不可能交给数据库服务期内部完成。无论什么类型的数据库,都不是为这种应用设计的。如果你硬要套用其它领域的应用模式的话,游戏服务器只能频繁的把各种数据从数据库中读出来,按游戏逻辑做出改变,再写回去。数据库变成了一个很低效的数据中转中心,无论你是否使用内存数据库,都改变不了这个低效的本质。

字符串,数组和关联数组(hash表)是最重要的三种数据结构,我们几乎可以利用它们模拟出任何更复杂的结构。Lua就是这么干的,只不过Lua把数组和关联数组合并成一个table类型了。D在语言层面对这三种数据结构支持的很好,概念定义非常清晰。这一篇只谈数组和字符串,不涉及hash表的部分。

数组可以看成是存放同一类型数据的连续内存。

在C语言中,数组和指针虽然是不同的类型,但编译器生成的代码却是相同的,可以说实质上,数组即指针。但将数组隐含有长度信息,即内存的范围。有些数组是固定大小的,在编译器就知道其范围;有些数组需要动态扩展大小,其范围是运行期确定,并可以改变的。无论如何,对数组的随机访问,缺乏边界检查的代码都隐藏着风险。

D语言是一门期望有高安全性的同时又重视运行性能的语言。它在平衡这个问题上的解决方案很有趣。程序员可以指定一段代码是安全的,还是系统级的,还是是接口安全的。根据不同的标注来插入边界检查代码。在debug版中,即使是系统级代码,也会插入类似assert的契约检查。

由于D语言以GC为内存管理核心(且要求所有数据都是位置无关,可移动的),所以管理数组切片Slice就变得很简单。不同的Slice引用同一块内存,不用担心数据生命期问题。扩展数组也可以根据需要重新分配内存,或是在原地扩展。

提到数组扩展,不得不谈一下D语言中结构的postblit。D语言中,所有的class都是引用语义的,而struct是值语义的。C++中花了很多年想解决的一个性能问题就是源于vector扩展时,数据如何从旧的位置移动新位置的问题。在stl的sgi实现中,为POD结构增加的特化模板来提高复制效率;在C++11中又从语言层面增加了右值引用来实现移动语义,来解决反复析构构造对象带来的性能浪费。

而D语言中没有那些晦涩的移动构造,拷贝构造概念;它只有postblit。也就是数据都应该默认按位复制(blit),然后在blit后,再用postblit方法去修改新的副本。这种不动源对象,而只在副本上修改的移动钩子技术概念更简单清晰。而且编译器可以自行推导什么时候调用postblit才是必要的。这个技术不仅仅用来解决数组的扩展问题,也可以很好的搞定C++中返回值优化问题。

对于固定大小的数组,D(2.0)是按值类型处理的(动态数组则是引用类型),不同长度的数组是不同的类型,但它们都可以隐式转换(映射)成动态数组。比较短的固定数组做值传递的时候更方便高效,也符合其它基础类型的特征。长数组可以通过ref修饰按引用传递。

但这种用法毕竟不够通用。

libphenom的string库核心想针对问题是尽量的减少堆上内存的动态分配。它把大部分临时字符串都放在栈上处理,也提供了用户自定义串空间的方法。我觉得这个方向是不错的,但是其实大可不必提供太多的弹性,只要尽量让临时字符串存在于栈上即可。而另一个很重要的功能,也就是stringinterning我认为更有实用性。

stringinterning可以实现symbol类型,对于类似json/xml的解析来说非常有意义。可以节约许多内存,而且可以加快symbol的比较和hash速度。不过对所有字符串无差别的做interning有可能因为外部输入多变也被攻击。对interning的字符串做引用计数也会降低性能。

最近听从同事建议想尝试一下MongoDB。

因为skynet需要一个异步库,不希望一个service在做数据库操作的时候被阻塞住。那么,我们就不可能直接把luamongo作为库的形式提供给lua使用。

一个简单的方法是skynet目前对redis做的封装那样(当然,skynet中的redis封装也是非阻塞的),提供一个独立的service去访问数据库,然后其它服务器向它发送异步请求。如果我直接使用luamongo就会出现一个问题:

我需要先把请求从luatable序列化,发送给和mongoDB交互的位置,反序列化后再把luatable打包成bson。获得MongoDB的反馈后,又需要逆向这个流程。这是非常低效的事情。如果我们可以直接让请求方生成bson对象,这样就可以直接把bson对象的指针发过到交互模块就够了(skynet是单进程模型,可以在服务内直接交换C指针)。这就需要我定制一套luamoogodb的driver了。

我觉得,这是因为一个对象,除了它自身的属性(例如大小、形状、颜色等)之外,还需要一些外部属性(例如位置、层次、方向等)需要逐级继承。每个对象都可以分解成更细的对象组合而构成、这些对象在组成新的对象后,它们的聚合体又体现出和个体的相似性(至少具有类似的外部属性)。这使得采用树状数据结构最容易描述它们。

树结构的基本操作无非是遍历整棵树、遍历一层分支、添加节点、移动节点、删除节点这些。但在大部分应用环境下,我们最多用到的只是遍历,而非控制树的结构本身。

最近稍微学习了一点Objective-C,做笔记和做编码练习都是巩固学习的好方法。整理记录脑子里的新知识有助于理清思路,发现知识盲点以及错误的理解。

以我这些天粗浅的了解,Objective-C似乎比C++更强调类型的动态性,而牺牲了一些执行性能。不过这些牺牲,由于模型清晰,可以在今天,由更先进的编译技术来弥补了。

我对C++的认知比Objective-C要多的多,所以对C++开发中会遇到的问题的了解也多的多。在学习Objective-C的过程中,我发现很多地方都可以填上曾经在C++开发中遇到的问题。当然,Objective-C一定也有它自己的坑,只是我才刚开始,没有踩到过罢了。

ObjC的类方法调用的形式,更接近于向对象发送消息。语法写作:

不过还是有一些泛泛的心得可以写写的。

前几天遇到一个优化的问题。我想采用定期计算路图的方式优化寻路的算法。而不用每次每个单位在想查找目标的时候都去做一次运算并记录下路径结果。一切都看起来很顺利,算法的正确性很快就被验证了。可是最后实际跑的时候,发现在生成路图的地方会稍微卡一下影响流畅性。

ps.在官方主页上,pixellight是基于OpenGL的,但实际上,它将渲染层剥离的很好。如果你取的是源代码,而不是下载的SDK的话,会发现它也支持了Direct3D。另,从2013年开始,这个项目将License改为了MIT,而不是之前的LGPL。对于商业游戏开发来说,GPL的确不是个很好的选择。

我相信这个东西已经被无数C程序员实现过了,但是通过google找了许多,或是接口不让我满意,或是过于重量.

我的需求是这样的:

蜗牛同学打算改进skynet增加异步IO的支持。

我今天在考虑现有的API时候,对比原有的timer接口和打算新增加的异步IO接口,发现它们其实是同一类东西。即,都是一个异步事件。由客户准备好一类请求,绑定一个sessionid。当这个事件发生后,skynet将这个sessionid推送回来,通知这个事件已经发生。

在用户编写的代码的执行序上,异步IO和RPC调用一样,虽然底层通过消息驱动回调机制转了一大圈,但主干上的逻辑执行次序是连续的。

受历史影响,我之前在封装Timer的时候,受到历史经验的影响,简单的做了个lua内callback的封装。今天仔细考虑后发现,我们整个系统不应该存在任何显式的回调机制。正确的接口应该保持和异步IO一致:

过年了,人都走光了,结果一个人活也干不了。所以我便想找点东西玩玩。

今天想试一下libev写点代码。原本在我那台ubuntu机器上一点问题都没有,可在windows机上用mingw编译出来的库一个backend都没有,基本不可用。然后网上就有同学推荐我试一下libuv。

libuv是node.js作者做的一个封装库,在unix环境整合的libev,而在windows下用IOCP另实现了一套。看起来挺满足我的玩儿的需求的。所以就试了一下。

其实铁路订票系统面临的技术难点无非就是春运期间可能发生的海量并发业务请求。这个加上一个排队系统就可以轻易解决的。

本来我在weibo上闲扯两句,这么简单的方案,本以为大家一看就明白的。没想到还是许多人有疑问。好吧,写篇blog来解释一下。

简单说,我们设置几个网关服务器,用动态DNS的方式,把并发的订票请求分摊开。类比现实的话,就是把人分流到不同的购票大厅去。每个购票大厅都可以买到所有车次的票。OK,这一步的负载均衡怎么做我就不详细说了。

每个网关其实最重要的作用就是让订票的用户排队。其实整个系统也只用做排队,关于实际订票怎么操作,就算每个网关后坐一排售票员,在屏幕上看到有人来买票,输入到内部订票系统中出票,然后再把票号敲回去,这个系统都能无压力的正常工作。否则,以前春运是怎么把票卖出去的?

我们来说说排队系统是怎么做的:

这半个多月其实做了不少工作,回想起来又因为太琐碎记不太清。干脆最近这几天完成的这部分工作来写写吧。

而之前的若干项目证明,其实没有良好的事务描述机制,并不是不可用。实现一个简单的RPC机制,一问一答的服务提供方式也能解决问题。程序员只要用足够多经验,是可以用各种土法模拟长流程的事务处理流。只是没有严格约束,容易写出问题罢了。那么这个问题的最小化需求定义就是:可以响应发起请求人的请求,解析协议,匹配到对应的处理函数。所有请求都应该可以并发,这样就可以了。至于并发引起的问题,可以不放在这个层次解决。

我谨慎的选择了RPC这种工作方式。实现了一个简单的RPC调用。因为大多数服务用Lua来实现,利用coroutine可以工作的很好。不需要利用callback机制。在每条请求/回应的数据流上,都创建了独立的环境让工作串行进行。相比之前,我设计的方案是允许并发的RPC调用的。这个修改简化了需求定义,也简化的实现。

举例来说,如果Client发起登陆验证请求,那么由给这个Client服务的Agent首先获知Client的需求。然后它把这个请求经过加工,发送到认证服务器等待回应(代码上看起来就是一次函数调用),一直等到认证服务器响应发回结果,才继续跑下面的逻辑。所以处理Client登陆请求这单条处理流程上,所有的一切都仅限于串行工作。当然,Agent同时还可以相应Client别的一些请求。

如果用callback机制来表达这种处理逻辑,那就是在发起一个RPC调用后,不能做任何其它事情,后续流程严格在callback函数中写。

每个RPC调用看起来是这样的:

最近工作有点感触,关于如何分工的。

我最近有所体会的还是那些被嚼过很多年的老道理。就是模块划分清晰,强内聚,低耦合之类。想强调的是,模块的层次一定要适中,同一层次上规模不能太大,有严格输入、输出接口。

这些并不是为了方便测试,检验工作正确性,而是为了拆分工作。

这个内存分配器需要是非入侵式的,即不在要分配的内存块中写cookie。

我吃完饭简单google了一下,没有立刻找到满足我要求的现成代码。心里估算了一下,C代码量应该在200行以下,我大概可以在1小时内写完。所以就毫不犹豫的实现了一份。

开始这个话题前,离上篇开发笔记已经有一周多了。我是打算一直把开发笔记写下去的,而开发过程中一定不会一帆风顺,各种技术的抉择,放弃,都可能有反复。公开记录这个历程,即是对思路的持久化,又是一种自我督促。不轻易陷入到技术细节中而丢失了产品开发进度。而且有一天,当我们的项目完成了后,我可以对所有人说,看,我们的东西就是这样一步步做出来的。每个点滴都凝聚了叫得上名字的开发人员这么多个月的心血。

技术方案的争议在我们几个人内部是很激烈的。让自己的想法说服每个人是很困难的。有下面这个话题,是源于我们未来的服务器的数据流到底是怎样的。

我希望数据和逻辑可以分离,有物理上独立的点可以存取数据。并且有单独的agent实体为每个外部连接服务。这使得进程间通讯的代价变得很频繁。对于一个及时战斗的游戏,我们又希望对象实体之间的交互速度足够快。所以对于这个看似挺漂亮的方案,可能面临实现出来性能不达要求的结果。这也是争议的焦点之一。

核心问题在于,每个PC(玩家)以及有可能的话也包括NPC相互在不同的实体中(我没有有进程,因为不想被理解成OS的进程),他们在互动时,逻辑代码会读写别的对象的数据。最终有一个实体来保有和维护一个对象的所有数据,它提供一个RPC接口来操控数据固然是必须的。因为整个虚拟世界会搭建在多台物理机上,所以RPC是唯一的途径。这里可以理解成,每个实体是一个数据库,保存了实体的所有数据,开放一个RPC接口让外部来读写内部的这些数据。

但是,在高频的热点数据交互时,无论怎么优化协议和实现,可能都很难把性能提升到需要的水平。至少很难达到让这些数据都在一个进程中处理的性能。

这样,除了RPC接口,我希望再提供一个更直接的api采用共享状态的方式来操控数据。如果我们认为两个实体的数据交互很频繁,就可以想办法把这两个实体的运行流程迁移到同一台物理机上,让同时处理这两个对象的进程可以同时用共享内存的方式读写两者的数据,性能可以做到理论上的上限。

ok,这就涉及到了,如何让一块带结构的数据被多个进程共享访问的问题。结构化是其中的难点。

方案如下:

这种设计很难让人做动态语言的binding,而大多数动态语言往往又没有强类型检查,采用生成代码的方式并没有特别的好处,反而有很大的性能损失(和通常做一个bingding库的方式比较)。比如官方的Python库,完全可以在运行时,根据协议,把那些函数生成出来,而不必用离线的工具生成代码。

这次,我重新做项目,又碰到protobuf协议解析问题,想从头好好解决一下。上个月一开始,我想用luajit好好编写一个纯lua版。猜想,利用luajit和ffi可以达到不错的性能。但是做完以后,发现和C++版本依然有差距(大约只能达到C++版本的25%~33%左右的速度),比我去年写的C+Luabinding的方式要差。但是,去年写的那一份C代码和Lua代码结合太多。所以我萌生了重新写一份C实现的想法。

现代语言为了可以接近玩乐高积木的那样直接组合现有的模块,都对模块化做了语言级别上的支持。我想这一点在软件工程界也是逐步认识到的。C语言实在是太老了。而它的晚辈Go就提供了import和package两个新的关键字。这也是我最为认可的方式。之前提到的方案只能说是对其拙劣的模拟。确认语言级的支持,恐怕也只能做到这一步了。

在项目实践中,那个USING的方案我用了许多年,还算满意。之前有过更为复杂“精巧”的方法,都被淘汰掉了。为什么?因为每每引入新的概念,都增加了新成员的学习成本。因为几乎每个人都有C语言经验,但每个人的项目背景却不同。接受新东西是有成本的。任何不是语言层面上的“必须”,都有值得商榷的地方。总有细节遭到质疑。为什么不这样,或许会更好?这是每个程序员说出或埋在心里的问题。

这两天我结合这半年学习Go语言的体验,又仔细考虑了一下这个问题。想到另一个解决方案。

今天侠少同学说“现在全文看下来还是有些纠结,反对、支持、再反对,再支持,百转千回的小情绪,读者恐怕会犯晕”。嗯,的确很羞愧的。不应该在这本大牛的书前面发牢骚。打算晚上改稿子。旧稿就贴这里存档吧。

就三天来实战经历,我喜欢上这门新语言有如下原因:

强类型系统。使得犯错误的机会大大降低。正确通过编译,几乎就没有什么bug了。而编写程序又有点使用lua这种动态语言的感觉,总之,写起来很舒服。

defer是个有趣使用的东西,用它来实现RAII比C++利用栈上对象的析构函数的trick方案让人塌实多了。go在语言设计上是很吝啬新的关键字的。但多出一个关键字defer,并用内建函数panic/recover来解决许多看似应该用exception解决的问题要漂亮的多。

zero初始化。我一直觉得C++的构造函数特别多余。按我用C的惯例,一切数据结构都应该用0初始化。所以C里有calloc这个函数。go把这点贯彻了。不会再有未定义的数据。

包系统特别的好。而且严格定义了包的初始化过程,即init函数。在我自己的C语言构建的项目中,实现了几乎一样的机制,甚至也叫init。但是有语言层面的支持就是好。对,只有init没有exit。正合我意。

goroutine是个相当有用的设计。8年前,我给C实现了coroutine库,并用在项目里,并坚信,程序就应该这么写。但是没有语言级的支持,用起来还是很麻烦。goroutine不仅简化了许多业务逻辑的编写,而且天生就是为并发编程而生的。select/chan可能是唯一正确的并发编程的模型。Erlang还是太小众了,而Go可以延用Erlang的模型,却有着纯正的C语言血统,我想会被更多人接受的。虽然Go依然可以用共享状态加锁的方案,但不推荐使用。chan用习惯了,还是相当方便的。

我觉得评注这个工作比翻译难做。作者细节上讲的非常清楚,大部分地方都不觉得有必要再加注解。我想跟这本书反复写了10年有关。所以很多页我都没留评注,真的不知道可以写啥。

编辑原想每页中英分列排版,我是不建议这样的。除了少部分评注,针对个别代码段,或关键词。大部分我的文字都是独立成段的。跟具体原文句子关系不大,只跟篇章段落主题有些许联系。

下面选一段贴出来吧。

可能还是因为我对C++偏见过多,有如前几年对其的推崇备至。总觉得书里讲的太细,太多观点本是好的,只是局限在了C++语言中。明明是C++的缺陷,却让人绞尽心力的回避那些问题,或是以C++独特的方式回避。在别的语言中不该存在的问题,却成了C++程序员必备的知识。人生苦短,何苦制造问题来解决之。

实际上,我为它提供的接口要更多一些,比如删除一个元素。

voidarray_erase(structarray*,seqiiter);原来的语义就是删除iter引用的元素。但这里引出一个问题:删除后,iter是否应该保持有效?

从语义上说,iter应该在调用完毕后变成一个无效引用。但实际应用中,往往需要在迭代array的过程中,删除符合条件的元素。让迭代器失效的做法,用起来很不方便。

有时候,我们需要把多个模块粘合在一起。而这些模块的接口参数上有少许的不同。在C语言中,参数(或是返回值)不同的函数指针属于不同的类型,如果混用,编译器会警告你类型错误。

在C语言中,函数定义是可以不写参数的。比如:

voidfoo();

这个函数定义表示了一个返回void的函数,参数未定。也就是说,它是个弱类型,诸如:

voidfoo(int);

voidfoo(void*);

这些类型都可以无害的转换成它。正如在C语言中,具体的指针类型如int*,char*都可以转换为void*一样。

注1:如果要严格定义一个无参数的函数,应该写成voidfoo(void);

注2:如果有部分参数固定,而其后的参数可变,则定义看起来是这样:voidfoo(int,...);这表示第一个参数为int,从第2个参数开始可变。

本篇是应《程序员》杂志约稿所写。原本要求是写篇谈C语言的短文。4000字之内。我刚列了个提纲就去了三千多字。-_-

现放在这里,接受大家的批评指正。勿转载。

今天晚上继续读《MastermindsofProgramming》,忍不住又翻译了半章关于Forth之父的访谈。我以前读过几篇更早时期关于他的访谈,部分了解他的观点。小时候还特别迷Forth。这位神叨叨的老头很有意思。

没看过原来的译本,只是自己按自己的理解翻了第4章Forth的前一半。我也算对Forth很有爱的人吧,也还了解Forth里诸如ITC(Indirected-threadedcode)这种术语到底指的什么,不过还是觉得翻译有点吃力。

对Forth同样有爱的同学们姑且看之吧。

setjmp是C语言解决exception的标准方案。我个人认为,setjmp/longjmp这组api的名字没有取好,导致了许多误解。名字体现的是其行为:跳转,却没能反映其功能:exception的抛出和捕获。

longjmp从名字上看,叫做长距离跳转。实际上它能做的事情比名字上看起来的要少得多。跳转并非从静止状态的代码段的某个点跳转到另一个位置(类似在汇编层次的jmp指令做的那样),而是在运行态中向前跳转。C语言的运行控制模型,是一个基于栈结构的指令执行序列。表示出来就是call/return:调用一个函数,然后用return指令从一个函数返回。setjmp/longjmp实际上是完成的另一种调用返回的模型。setjmp相当于call,longjmp则是return。

重要的区别在于:setjmp不具备函数调用那样灵活的入口点定义;而return不具备longjmp那样可以灵活的选择返回点。其次,第一、setjmp并不负责维护调用栈的数据结构,即,你不必保证运行过程中setjmp和longjmp层次上配对。如果需要这种层次,则需要程序员自己维护一个调用栈。这个调用栈往往是一个jmp_buf的序列;第二、它也不提供调用参数传递的功能,如果你需要,也得自己来实现。

我们设计任何一个模块,都应当对其实现细节尽可能的隐藏。只留下有限的入口和外部通讯。这些入口如何定义是重中之重。大多数情况下,我们都在模仿已有的系统来设计,所以对貌似理所当然的接口定义不以为然,以为天生就应该是那样,而把过多精力放在了如何做更好(更优化)的实现上。对接口设计方面缺乏深度的思考,使得在面对新领域时,或是随心所欲,或是不知所措。

即使是有成熟设计的模块,用户依然可能使用错误。模块的设计者不能要求模块的使用者对其内部实现了然于胸。不能指望自己能写出完善的文档去告诫使用者,当你怎样用的时候会用错。即使写出了完善的文档,也不能指望每个人都仔细的读过。就算读了,也有百密一疏的时候。越是专有的模块,越不能指望文档或是口述的教导,更不能指望程序员去精读源码。人生苦短,如无特别的理由,逐行去读同僚的代码,何不自己重写一遍?你设计的系统若用出了问题,与其怪别人用错,不如怪自己没设计好。MSDN洋洋洒洒文字以G计算,也属无奈之举。依然有无数人犯下那些被人提及的错误。

现举一个一切C语言程序员都用过的模块的设计:内存管理模块。

标准API为三个:

malloc

free

realloc

大多数程序员都以为这理所当然。直到接触到gc的方式管理内存,方知另有一片天地。即使在C库中引为标准,也并不是所有内存管理器都承认这种简洁的。比如在Windows的API中,HeapAlloc系列的内存管理模块的API就更复杂一些。

Windows游戏软件在发布时,通常会把所有数据文件打包。这通常出于两个目的:一是保护数据文件不被最终用户直接查看,二是Windows的文件系统一度相对低效。尤其是在处理非常多小文件的时候,无论是安装、分发还是运行时处理都有性能问题。

而游戏软件通常会有大量的资源文件,对数据文件打包的需求更为强烈。一般游戏引擎都会支持至少一种资源打包的形式。

数据结构的序列化是个很有用的东西。这几天在修改原来的资源管理模块,碰到从前做的几个数据文件解析的子模块,改得很烦,就重新思考序列化的方案了。

Java和.Net等,由于有完整的数据元信息,语言便提供了完善的序列化解决方案。C++对此在语言设计上有所缺陷,所以并没有特别好的,被所有人接受的方案。

现存的C++serialization方案多类似于MFC在二十年前的做法。而后,boost提供了一个看起来更完备的方案(boost.serialization)。所谓更完备,我指的是非侵入。boost的解决方案用起来感觉更现代,看起来更漂亮。给人一种“不需要修改已有的C++代码,就能把本不支持serialize的类加上这个特性”的心理快感。换句话说,就是这件事情我能做的,至于真正做的事情会碰到什么,那就不得而知了。

好吧,老实说,我不喜欢用大量苦力(或是高智慧的结晶?)堆积起来的代码。不管是别人出的力,还是我自己出的。另外,我希望有一个C的解决方案,而不是C++的。

“...MarkLinton顺便到我的办公室来了一下,提出了一个使人印象深刻的请求,要求提供第三个控制层次,以便能支持斯坦福大学正在开发的Interviews库中所使用的风格。我们一起揣测,创造出单词protected以表示类里的一些成员,...”

“...Mark是Interviews的主要设计师。他的有说服力的争辩是基于实际经验和来自真实代码的实例。...”

“...大约五年之后,Mark在Interviews里禁止了protected数据成员,因为它们已经变成许多程序错误的根源...”

我不喜欢protected,但是今天,我偶尔用一下C++时,不再有那么多洁癖。反正很难用C++做出稳定的设计,那么,爱怎么用就怎么用吧。关键是别用C++做特别核心的东西就成了。

今天,碰到一个跟protected有关的问题,小郁闷了一下。觉得可以写写。这个倒是个基本问题,貌似以前很熟悉。毕竟很多年不碰了,对C++语法有点生疏。

但是,某些场合下,采用面向对象的确是比较好的方案。比如UI框架,又比如3d渲染引擎中的场景管理。C语言对面向对象编程并没有原生支持,但没有原生支持并不等于不适合用C写面向对象程序。反而,我们对具体实现方式有更多的选择。

大部分用C写面向对象程序的程序员受C++影响颇深。企图用宏模拟出一个常见C++编译器已经实现的对象模型。于我愚见,这并不是一个好的方向。C++的对象模型,本质上是为了追求实现层的性能,并直接体现出来。就有如在C++中被滥用的inline,的确有效,却破坏了分离原则。C++的继承是过紧的耦合。

我所理解的面向对象,是让不同的数据元有共同的操作方式,适合成组的处理。根据操作方式的不同,我们会对数据元做不同的分组。一个数据可能出现在这个组里,也可以出现在那个组里。这取决于你从不同的方面提取的共性。这些可供统一操作的共性称之为接口(Interface),接口在C语言中,表现为一组函数指针的集合。放在C++中,即为虚表。

我所偏爱的面向对象实现方式(使用C语言)是这样的:

这几天白天都在安排面试,其实还是有点累的。晚上就随便写点程序,好久没摸C++,有点生疏。也算是娱乐一下吧。

主要工作其实是在C库的基础上做一个C++的中间层。跟在C库的基础上做lua中间层差不太多。前几天加入了gc后,发现了一些有趣的用法。

比如对于构造对象。C的api中,如果创建一个对象失败,就会返回空指针。但是对于C++就不一样了,new是不应返回空指针的。书本上的推荐做法是在构造函数里抛异常。但是我又不太想进一步的引入异常机智,怎么办呢?

为这篇blog打腹稿的时候,觉得自己很贱,居然玩弄C++起来了。还用了template这种很现代、很有品味的东西。写完后一定要检讨。

说起COM,我脑子里就浮现出各种条条框框。对用COM搭建起来的Windows这种巨无霸,那可真是高山仰止。套dingdang的popo签名:虽不能至,心向往之。

好吧,我琢磨了一下如何解决下面的问题,又不把虚继承啦,虚析构函数啦之类的暴露在接口中。

简单说,我有几个接口是一层层继承下来的,唤作iAiB。iA是基类,iB继承至iA。

然后,我写了一个cA类,实现了iA接口;接下来我希望再写一个cB类,实现iB接口。但是,iB接口的基类iA部分,希望复用已经写好的cA类。我想这并不是一个过分的需求。正如当年手写COM组件时,我对手写那些AddRefReleaseQueryInterface深恶痛绝。

用虚继承可以简单的满足这个需求:

我好多年没写C++程序了,读C++代码也是偶尔为之。

今天晚上就碰到这么一个诡异的问题,我觉得是我太久没摸C++了,对那些奇怪的语法细则已经不那么熟悉了。有知道的同学给我解惑一下吧。

事情的起因是,我想安装一个perl模块唤作Syntax::Highlight::Universal。

本来用CPAN安装很方便的,直接install即可。

可是在我的机器上,make死活通不过。我就仔细研究了一下编译出错信息。又读了一下源代码,自己感觉没错。纠结了半天,仔细模仿出错的地方写了一小段程序测试。

今天继续谈模块化的问题。这个想慢慢写成个系列,但是不一定连续写。基本是想起来了,就整理点思路出来。主要还是为以后集中整理做点铺垫。

我们都知道,层次分明的代码最容易维护。你可以轻易的换掉某个层次上的某个模块,而不用担心对整个系统造成很大的副作用。

层次不清的设计中,最糟糕的一种是模块循环依赖。即,分不清两个模块谁在上,谁在下。这个时候,最容易牵扯不清,其结果往往是把两者看做一体去维护算了。这里面还涉及一些初始化次序等繁杂的细节。

其次,就是越层的模块联系。当模块A是模块B的上层,而模块B又是模块C的上层,这个时候,让模块C对模块A可见,在模块A中有对C导出接口的直接调用,对于清晰的设计是很忌讳的一件事。虽然,我们很难完全避免这个问题,去让A对C的调用完全通过B。但通常应尽力为之。(注:以后写书的话,我争取补充一些实际的例子来说明)不过,对语言不原生支持的数据类型,以及基础设施,但却有必要创造出来给系统用的。可以有些例外。比如内存管理,log管理,字符串(C语言用原始库函数管理比较麻烦)等等,我们可能以基础模块的形式提供。但却可能被不同层次的模块直接使用。但,上到一定层次后,还是需要去隐藏它们的。

下面来一点更实际的分析。

继续昨天的话题。随便列些以后成书可能会写的东西。既然书的主题是:怎样构建一个(稍具规模的)软件。且我选择用C为实现工具来做这件事情。就不得不谈语言还没有提供给我们的东西。

模块化是最高原则之一(在《Unix编程艺术》一书中,Unix哲学第一条即:模块原则),我们就当考虑如何简洁明快的使用C语言实现模块化。

除开C/C++,在其它现在流行的开发语言中,缺少标准化的模块管理机制是很难想象的。但这也是C语言本身的设计哲学决定的:把尽可能多的可能性留给程序员。根据实际的系统,实际的需要去定制自己需要的东西。

对于巨型的系统(比如Windows这样的操作系统),一般会考虑使用一种二进制级的模块化方案。由模块自己提供元信息,或是使用统一的管理方案(比如注册表)。稍小一点的系统(我们通常开发接触到的),则会考虑轻量一些的源码级方案。

觉得上一本没写好,是因为还是太仓促了。即使是已经写的那点东西,也积累不够。写书的经验也太少。我倒不怕有错误被人骂,是怕自己回头不满意。

如果再写,肯定只抓着很少的问题谈。但是具体写什么,还没全想好。积累是有了点,真能够拿出来写写的不多。毕竟,写书和写blog瞎扯还是不一样的。

由于最近几年用的主要开发语言是C和lua。那么也打算以此为基础写。假定读者至少有不错的C语言基础了。我真正想谈的是,如何把一个软件很好的构建起来。到底需要做些什么。(从实现层面看)怎样才是好的软件。

那么有一个重点问题,也是老问题,怎样才是好的设计。

正如引言中所述:”与主流观点相反,从根本上说,最普通形式的面向对象程序要比对应的面向过程的程序更难测试和校验。“

Lippman大牛的第一场,关于大型可伸缩性的软件开发的,ChenShuo同学翻译的很不错:D

找到电源,所以可以写写了。

果然是牛人啊,上来就讲形而上的东西。我听的有趣,就做了点笔记,但是记的不多。

我们从自然界去寻找灵感,然后在计算机领域去搞出来。以前的计算机是没有内存的,后来冯大侠说,计算机就像大脑,大脑是有记忆的,所以有了内存。

我们现在说大脑就像计算机,是本末倒置了。人们总是从自然界的角度来思考,然后解决软件里的问题。Lippman牛的想法是,把软件比作生物,从DNA,细胞核开始向上一层层的。

系统的基础组织部分是DataStructure和DataStream,这个就像细胞一样;在应用领域方面,ExecutiveFunction和TypeInformation就好比生物的各个器官。

我们面临的问题,不是语言和库为我们提供了多少可能。而是我们无法确定,我们到底至少需要什么?

我这几年一直用C在做设计和编码(当然还写了许多Lua程序)。也一直在纠结,到底,我们在系统编程中,最少的需求是什么。C++给了我们太多,但是C又给的太少。如果常年用C写程序,那么必须多一点。

因为Lua用的太多,感觉字典是个很有用的东西,所以我实现了一个基本的hashmap。

一个可以方便的从两头插入,从两头删除,可变长,可以高效随机访问的sequence。在《C语言接口与实现》里把这个东西简写为seq,在Haskell里也是。

为什么我一直从心里排斥把seq做成独立模块,我想是我的追求高效的坏习惯在作祟。虽然这个习惯很不好,但是我总觉得明明可以用一个原生数组来做这件事情,用个指针就可以遍历了,何必多几次函数调用,和数据访问的间接性呢。

固然,C++的STL提供了一个折中的方案,把代码都写到.h里(使用template),利用编译器去优化inline代码。但是却在源代码中暴露了数据结构的实现细节。今天的我看来,这是更不可以容忍的事情。(不同意这个观点的同学请原谅我的偏执)

不支持closure的语言用起来真是太难受了。

给语言加新特性并不可怕。因为我们最终是要用语言解决问题的。

云风对那种所有成员数据都写setter/getter的做法有什么看法吗……这两天试图精简三个太庞大的类,但是单单setter/getter就让接口数目变得非常多了……

我谈谈我的看法吧。

首先,几乎任何设计问题都没有标准答案。如果有,就不需要人来做这件事了。无论多复杂的事情,只要你能定义出精确的解决方案,总可以用机器帮你实现。

下面谈谈我的大体设计原则。记住、一切皆有例外,但这里少谈例外。因为这涉及更复杂的衡量标准。

要写过多少代码才能得到哪怕一点真谛?

多少年过来,我在潜意识的去追求复杂的东西。比如我自幼好玩游戏,从小到大,一直觉得玩过的游戏过于简单(无论是电子游戏还是桌面游戏),始终追寻更复杂规则的游戏,供我沉浸进去。或许是因为,有了更高的理解和控制复杂度的能力,就可以更为轻松的驾御复杂性。

这很好的解释了2000年到2004年我对C++的痴迷。还有对设计模式的迷恋。

EricS.Raymond说:尽量不要去想一种语言或操作系统最多能做多少事情,而是尽量去想这种语言或操作系统最少能做的事情——不是带着假想行动,而是从零开始。禅称为“初心”(beginner'smind)或者叫“虚心”(emptymind)。

代码写多了,问题见过了,甚至是同一问题解决多了。模式这种东西自在心底,不必拿出来。时时的从零去想,总能重新明白一些道理。

为什么说语言重要也不重要,算法和数据结构重要也不重要。对要解决的问题的领域的理解很重要(即明白真正要做什么)。理解了,我们才可以用面向对象,用模式去套问题;可理解了,我们又不真的需要这些繁杂的抽象。

闲话放一边,今天想谈谈树结构的管理。

《Unix编程艺术》中总结的Unix哲学中有这么一条:除非确无它法,不要编写庞大的程序。并且在第13章花了一章讨论复杂度的问题。(第13章复杂度:尽可能简单,单别简单过了头)

下周一,是我们项目的一个进度线。所以,周末我安排了加班。当然,我最近两年,每个休息日都给自己安排的加班,无所谓周末。不过给团队安排加班还是比较稀少的事情。

把同质的东西放入一个容器,然后用迭代器迭代这个容器,把里面的内容逐个取出来处理。这是一个非常常见的需求。但是,这个过程往往也会滋生bug。因为,若将容器看成一个对象,那么对其迭代的这个操作很难实现原子性。

非原子性导致了,在迭代过程中,十分有可能对容器本身进行修改。或增加若干元素,或删除若干元素。这些都容易造成迭代过程不正常。

所以,最终我们需要根据需求设计以及实现合理的容器。比如管理消息的消息队列,严格的满足尾进头出,没有删除中间数据的需求,就不会导致bug。

那么,如果容器是一个集合怎么办?即,允许向其中增加新的元素,也可以移除某些元素。这种数据结构非常有用。比如向某对象注册若干回调函数,一旦满足条件则依次调用。即设计模式中的Observer观察者模式。回调函数就极有可能增加新的观察者或某些老的观察者退出。

嗯,那段代码写的比较乱,居然真有人读完了,我真是佩服的紧啊。:D

这是第一篇,在那里还有A-K,有兴趣的朋友可以自己去看。

牛人写的计算机方面的书,我想有两个极端。要么像高爷爷的TAOCP,厚厚的几大本,还在待续;要么如K&R的TCPL,薄薄一本,躺在床上轻松读完。

之前,我从没想过,以C语言为主题的书,可以砖头样的一大本,1600+页。用80克纸双面打印,会厚达9.2厘米(作者语)。不过看过副标题,稍微可以理解:AnEconomicandCulturalCommentary。感情上升到经济和文化了。敢用这个词的人绝非泛泛之辈。

一不小心读到凌晨五点,居然Introduction的一半还没读完:(,也就是说,前100页都还没正式谈C语言呢。

由于是英文版,阅读速度受到了极大的限制。而行文中大量我不认识的单词和长句,迫使我不断的查字典。就这样还能坚持读下来,只能说,这是一本好书,有趣的书。

下面,我试着将前面介绍部分选译几段,鉴于原文的行文与我来说有许多艰深之处,恐怕很难译的准确,姑且看之吧。

C语言在本质上,参数传递都是值传递。不像Pascal和C++可以传引用。这一点,使得C语言可以保持简单的设计,但另一方面也颇为人诟病。

因为性能问题,指针不得不被引入。可以说,用C语言实现的软件,其实现的Bug90%以上都来至于指针,应该是没有夸大了。当然设计引起的问题或许更为关键一些,那些于指针无关。

原来以为这种小东西没多少代码量的,本地编译就够了。可惜我轻视了C++代码编译的龟速。在LSPro上,居然一个短短的.cc文件就要编译20秒左右。有点后悔没有用交叉编译,不过忍了几小时也就过去了。

随着engine开发进入尾声,最近几个月已经在修一些边角的东西,顺便给其他组的同事做介绍和教学。由于不在一起办公,折腾VPN和防火墙也折磨了好几天。

由于资源管理模块当初设计的还是比较仓促,有些需求到了后期才逐步出现。比如,我们需要同时使用不同的资源加载模块,分别从本地文件系统于打包文件中读取。一开始,我认为,开发期不将资源文件打包是可以的。随着开发的进展,数据文件在数量级上增加,感觉将一些不太变化的文件打包还是有必要的。这就必须实现混合加载。

前期考虑到要做数据预读,我们将文件之间的依赖关系放到了文件系统内。而非自己定制的数据包文件系统和本地文件系统间有很大的不同。这使得混合加载实现比较困难。(具体原因是,我们的文件系统内采用的类似linuxext2的思路,每个文件有一个inode标识,并描述相互关系。但文件却不一定有文件名。这和本地文件系统相异,导致相互依赖关系难以描述。)

第二,虽然前期设计为多线程加载考虑。但由于中间变化很多,例如需要在加载过程中把部分数据上载到显存。最终,实现多线程安全变的很困难。(具体原因是,每种文件类似都有自解释的代码,我们的客户端主体engine代码是按单线程设计,实现资源加载的多线程安全必然对以后扩展资源文件类型的程序员做过多限制)

基于以上两点,我觉得花一番气力对整个资源管理模块做一次大的重构。

为了不影响项目组其他人员的工作,估算了工作量后,我决定自己在中秋节做这次重构工作。并在假期结束可以顺利归并到代码基的主干。

事情没有预期的顺利。

嗯,首先,此贴不是牢骚帖。

dt2用了大量的lua代码构建系统,但从系统设计上,沿袭了老的大唐的许多代码。原来的大唐是用C++构建的,为了利用上这些代码(虽然我觉得这种复用非常无意义,但是其中原因复杂,就不展开谈了),dt2engine的开发人员做了一套非常复杂的中间层,把lua和C++几乎无缝的联系在了一起。

Keepitsimple,stupid.

stupid不是愚蠢的愚,而是大智若愚的愚。有时候,我们写程序做设计就是不够智慧,就只有点聪明。觉得自己可以把复杂的问题用巧妙的方法解决。这个巧妙的方法,保正了效率,节约的内存。真可谓聪明。

但聪明不是智慧,真正的智慧是看到将来。在不断的演化中它们还能不能保持这个巧妙。如果不那么巧妙,我们到底会损失些什么,我们真正需要什么,而失去的那些换来的到底是什么。

simple也不是为了避免麻烦。保持simple比解决麻烦要麻烦的多。

嗯,那么3d引擎是什么?跟3dapi(Direct3D或openGL)有什么区别?固然,engine如果只是做3dapi的一层薄薄的封装,抹平各套3dapi的差异。那么,就过于底层,显得小了。

如果为特定形式的游戏写死代码,让开发者写一些MOD插件就可以形成不同的游戏,那么又显得太高。在这种高层次上,游戏类型会限制于engine的实现。比如魔兽争霸3就直接用户写MOD,并的确有人以此发展出许多玩法。但你永远不可能在魔兽争霸3的基础上写一个MOD实现第一人称射击游戏。

所以我指的3dengine,是处于3d游戏软件结构中间地位的东西。

那么,我们的3dengine到底要解决的是什么问题?做engine绝对不是以我能在3dapi的基础上扩展出什么东西为设计向导。因为,对于完成一个软件,是一个从机器实现域映射到问题域的过程。这两个领域的模型是不同的。3dapi完成的是实现域的扩展,engine则应该完全从实现域到问题域的一个变换,让开发者可以用最接近问题域的语言来表达问题。

面向对象方法被人谈论了二十多年了。我接触它比较晚,直到九十年代中期才开始学习使用它。若说对这个方法做些评价,那还真是大言不惭了。不过这么些年来,也周期性的对面向对象做些思考。或对或错,我想都值得总结一下。一家之言,来看的同学不必太当真。

首先我们要区分一下“基于对象”和“面向对象”的区别。

基于对象,通常指的是对数据的封装,以及提供一组方法对封装过的数据操作。比如C的IO库中的FILE*就可以看成是基于对象的。

本质上来说,引用计数策略和垃圾收集策略都属于资源的自动化管理。所谓自动化管理,就是在逻辑层不知道资源在什么时候被释放掉,而依赖底层库来维持资源的生命期。

而手工管理,则是可以准确的知道资源的生命期,在准确的位置回收它。在C++中,体现在析构函数中写明delete用到的资源,并由编译器自动生成的代码析构基类和成员变量。

所以,为C++写一个垃圾收集器,并不和手工管理资源冲突。自动化管理几乎在所有有点规模的C++工程中都在使用,只不过用的是引用计数的策略而非垃圾收集而已。也就是说,我们使用C++或C长期以来就是结合了手工管理和自动管理在构建系统了。无论用引用计数,还是用垃圾收集,软件实现的细节上,该手工管理的地方我们依旧可以手工管理。

粽子节假期,欧洲杯开战。为了晚上不打瞌睡,我决定写程序提神。这三天的成果就是:实现了一个C用的垃圾收集器。感觉不错。

我希望做一个更纯粹的gcforC/C++模块,接口保持足够简单。效率足够的高。三天下来,基本完成,正在考虑要不要放到sourceforge上开源。等过两天彻底测试过再做打算(或许再支持一下多线程收集)。

下面列一下设计目标和实现思路。

其实我并没有用lua亲手写过什么大规模的项目。超过5千行代码的项目几乎都是C或是C++写的。这几天算是做了点复杂的玩意了。几经修改和删减,最后接近完工的这个东西统计下来不多不少3000行(误差在十位数)。其中用C编写了基础模块900多行(仅仅是socketapi的封装和byte流的编码解码),剩下的都是用lua设计并实现的。

前几天跟同事闲聊64位操作系统时,有人问起64位平台上,C语言的数据类型如何确定的问题。以及跨平台(跨16位、32位和64位平台)程序如何选用合适的数据类型。

我查了一下资料,记录如下:

char通常被定义成8位宽。

int通常被定义成16位或32位宽(或更高),它取决于平台。编译器将在这两者间选择最合适的字宽。

short通常被定义成16位宽。

long通常被定义成32位宽。

C99为C语言扩展了新的整数类型longlong,通常被定义成64位宽。(GNUC亦支持)

但是C标准并没有定义具体的整数类型的宽度,只定义了longlong的级别高于long,long的级别高于int,int的级别高于short,short的级别高于char。(另外有_Bool永远是最低级别)。级别高的整数类型的宽度大于等于级别较低的整数类型。

char的宽度用宏CHAR_BIT定义出来,通常为8,而除了位域类型外,其它所有类型的位宽都必须是它的整数倍。

如果需要精确确定整数类型的宽度,在C99以及GNUC中,需要包含stdint.h,使用其中几种扩展的整数类型。

昨天谈到了对象生命期管理的问题。我们来看操作系统是怎么管理资源的。

对于资源的集合体,操作系统抽象出进程的概念。每个任务可以向系统索取资源,操作系统放在进程的集合内。进程在,资源在;进程死,资源收回。从操作系统看出去,一个个对象都是独立的,不用理会相互的依赖关系,有的只有对象handle。收回这些对象的次序是无所谓的,跟发放他们的次序无关。

这里比较重要且特殊的是内存资源,操作系统其实不直接发放物理内存给用户,用户看到的只有虚拟地址空间。真正分配出去的是地址空间。而且空间是按页分配的,到了用户那里,再由用户自行切割使用。

这么看,内存管理的确是最复杂的部分。因为用户通常不能像文件handle那样,拿来是什么还回去还是什么。一个简单的引用记数就可以管理的很好。内存资源必须做多层次的管理。或许未来64位系统普及后,这个问题会简单很多,但谁叫我们主流应用还是跑在32位平台上呢?而且64位系统未必不会出现新的问题。我们现在看64位系统,估计跟当年在dos实模式下写程序时曾经幻想以后随随便便就有4G内存用的感觉一样。

除去资源管理,操作系统通常都会抽象出线程这个代码执行流程,加以统一管理。线程本身会作为一种资源放在进程的管理集合中。但是操作系统又需要对所有线程的集合做统一的调度。从这个角度看,仅仅分层归组管理是不够的。

其实不仅是线程,像socket这样的资源同样不能简单置于进程的层次之下。一个tcp连接是不能简单的在进程结束后直接干脆的抹掉。另外负责网络通讯的核心模块也需要有轮询系统中所有socket的能力。

综上看来,对象的生命期管理在同一层次上似乎应该有交叉的两条线。一条是拥有共同的生命期的集合;另一条是同类对象的集合。

先不忙下结论,再谈谈我们现在自己设计的引擎用到的一些管理策略和最近发现的一些不足吧。

这两周过的很混乱,主要是从程序部分脱出来,在写游戏的策划案。没怎么写代码,人有点空虚。策划案都是文字活,脑子里想是一回事,写出来又是回事。还有很多细节似乎是因为没想明白,所以表达不清。还得努力。

今天写这么一篇,倒不全因为有美女鼓励。其实在下午百无聊赖的时候就想敲点什么了,一摸键盘又觉得没想清楚。在blog管理界面里已经有好几篇这样的稿子,写完了就那么放着而没有公开。生活若不是为了生存,那么就自然会充斥着胡思乱想,这些年我就这么个状态。偶尔想明白点什么,就写下来。而更多的,来也也快去的也快。

其实最开始想写的还是技术上的东西,大致有两点。

昨天在写一个AOI模块,设计时又碰到一个对象迭代的老问题,必须谨慎对待,文以记之。

缘起:

当对象A进入B的AOI区域时,会触发一个Enter事件。这个事件处理是以回调函数的形式完成,如果回调函数中再次调用AOI模块,产生一次间接递归,就有可能破坏AOI模块内部的某些迭代过程。

这类问题在各种对象管理的程序框架中经常出现。除了上面提到的AOI模块,在GUI模块设计中也极其常见。下面谈谈我的解决方案吧。

语言之争永远是火药味十足的话题。尤其是C和C++的目标市场又有很高的重合性,C++程序员往往对C++其有着宗教般的虔诚。我想,今天我在自己的blog上继续这个战争,一定会换来更多的骂名。只不过这次Linus几句话真是说到我心坎里去了,不喊出来会憋坏的:D

既然C++是你的工具,你就应该努力把自己对工具的改进需求说出来。

语法层面的(nontrivial的)错误往往预示着语意层面的错误。例如,循环依赖导致的语法错误往往暗示抽象设计存在问题。

这些是我最近用C写了很多代码后悟到的。

组件式的设计中,最难处理的就是模块的初始化次序和退出次序的问题。如果不考虑动态加载卸载问题,则可以简化一些设计。

退出问题解决起来最为容易,安全的退出唯一需要考虑的是对系统资源的释放。这是操作系统的工作,进程被杀掉时,所有占用的资源都会被安全回收,所以我选择的是不作为。

初始化部分要相对复杂一点,我把模块加载和初始化部分分开。加载的过程是静态的,无相互依赖关系的。实际上,做的只有代码和数据段载入内存的过程。(在实现上,要绝对避免隐式的代码运行,例如C++中的全局对象的自动初始化)

初始化过程是惰性的,即:用到再调用初始化入口。每个模块都在自己初始化的时候去调用所依赖模块的初始化(实际上也必须如此,否则拿不到别的模块的句柄,无法使用其它模块内的方法),这样模块之间的初始化次序就自然规整了。只要不人为写出循环初始化的逻辑,是不会出错的。

实际操作中遇到一个问题,某些模块的初始化依赖一些参数。当需要参数传递的时候,初始化流程就变的复杂了。昨天同事提出一个需求:3d渲染的基础模块需要一个窗口句柄来初始化自己,否则无法使用。那么依赖这个渲染模块的其它模块的初始化部分就必须也知道这个窗口句柄。而窗口句柄是由窗口管理模块初始化后,构造一个窗口才能得到的。其它模块均无法自行构造出窗口来。

我们上一个版本的设计中,模块管理器拥有一快公有数据区,专门用于模块初始过程的数据交换。这种类似Windows注册表的设计,隐隐的,一直让我觉得不妥。这次重构代码时,就把它从设计中拿掉了。

重构代码到今天,发现该碰到的问题依旧存在,需要想办法更好的解决这个问题。昨天晚上躺在床上把怪物猎人2中的轰龙一举干掉,一扫几天来的郁闷心情。突然来了灵感,找到一个很简洁的方案解决这个问题。

第二个目标昨天达成了。觉得整个过程还是攒了些经验,值得写写。那就是“怎样才算设计良好的模块”。这个话题比较大,几次想总结经验都不敢下笔。

这个题目前辈已经论述的太多,而自己的感悟一但落到文字就少了许多东西,难免被方家取笑。

最正确的道理永远是简单的,却因为其简单,往往被人忽略。程序员还是要靠不停的写新的代码,以求有一天醍醐灌顶:原来自己一直懂的简单道理,其实才刚刚理解。

昨天试着维护几年前写的一个C++库,只是增加一点东西。以那个时候我的眼光看,这个库设计的不算太差。这几天用的时候需要一些新功能,修改代码的话,有点担心引起不必要的麻烦。所以我决定从其中一个类继承下来,增加我要的东西。

元旦这几天没啥心情做项目,老是胡思乱想。

这几天想走另外一条路子:如果自己写一个C语言的前端,好象当年C++干过的那样。该怎么实现最为合理呢?

以前版本的lua缺省是调用的crt的内存分配函数来管理内存的。但是修改也很方便,内部留下了宏专门用来替换。现在的5.1版更为方便,可以直接把外部的内存分配器塞到虚拟机里去。

有过C/C++项目经验的人都知道,一个合适的内存分配器可以极大的提高整个项目的运行效率。所以sgi的stl实现中,还特别利用freelist技术实现了一个小内存管理器以提高效率。事实证明,对于大多数程序而言,效果是很明显的。VC自带的stl版本没有专门为用户提供一个优秀的内存分配器,便成了许多人诟病的对象。

其实以我自己的观点,VC的stl(我用的VC6,没有考察更新版本的情况)还是非常优秀的,一点都不比sgi的版本差。至于allocator这种东西,成熟的项目应该根据自己的情况来实现。即使提供给我一个足够优秀的也不能保证在我的项目中表现最佳,那么还不如不提供。基础而通用的东西,减无可减的设计才符合我的审美观。sgi版stl里的allocator就是可以被减掉的。

好了,不扯远了。今天想谈的是,如何为lua定制一个合适的内存分配器。

摘录的一段:

Myexperiencefromotherlanguages(C,C++,ObjectiveC,Java,Smalltalk,etc)suggeststhattryingtoaddsomenotionofconsttothelanguageisdetrimental.It'srarelyofusetothecompiler,addslittleornosafetybarrierforprogrammers,isconfusing,andisn'talwayshelpfuldocumentation.I'veseengrownmenweepoverC'sconst,andhighlypaidC++professionalsargueforhoursoverthemeaningofconstinC++.

Therightplacetoannotatethingswithnotionsofconstantnessisthetypehierarchy.See,forexample,java.awt.Raster/java.awt.WritableRaster(fromJava's2Dapi),NSDictionary/NSMutableDictionary(fromObjective-C'sfoundationclasses).

drj

以为然也

当一个对象被很多地方引用的时候,通常我们会给出引用记数,当记数减到0的时候就删除,这是个看似完美的解决方案。但是,有多少地方会记得解除引用呢?借助C++的语法糖,可以自动的完成这些工作。长期的引用关系,可以在构造和析构的时候操作;短期的引用,比如就在一个函数内获得对象,操作完毕后马上解除引用。这个时候,可以通过返回几个warpper对象来完成。

最近在用C写程序,规模不小的程序,也谈不上太大。大约一万行之内的模块吧,关于UI的基础框架。我知道这个东西连用C++都谈不上合适,更莫谈C了。可是我倔强的认为,应该用C把它写好。

很多人批评C++没有拥有好的教育体系和方式,导致了很多C++程序员在用C的方式写C++,或是把C++当成更好的C来用。可是受过C++(正确的?)教育熏陶过的程序员呢,当他拿起C的时候,是否把C当成蹩脚的C++来用呢?

我希望我不是。

了解了更多的语言后,我深信,每种语言有它的游戏规则。C有C的规则,C++有C++的规则。用的越深入,越发觉得其间的差别。C++不是C,C也不是C++。它们的最大的共通点,是类似的语法,语法类似到可以用一套编译器编译。

我们的引擎的最初设计是unicode的,后来决定同时支持unicode和multibyte。所以我在jamfile里设置了一个叫做unicode的feature可以开关,这样我就可以得到两个版本。

但是,全部使用unicode又不太现实,有些系统提供的东西的接口就没有unicode版本,例如fx脚本。那么我们必须在unicode版本中又用回某些模块的multibyte版本。况且我们的引擎是跨平台的,不是所有的平台都像Windows这样对unicode支持的很好。管理两个版本本来就是一件非常麻烦的事情。

最初,作为一个die-hard的C++程序员,我曾经是很瞧不起java的。不知道时候还有朋友保留我在5年前,作为一个自由程序员印过的私人名片。当时便直接把自己对java的不屑一顾签写在上面。

日子过了好久,人的思想也在一步步改变。在我写那本书的时候,对java的态度已经好了许多。至于现在,离那些文字又过去一年,java于我,可以说很有好感的,了解也逐渐增多。

今天,脚本编译器连同前段写的虚拟机全部完工了,很有成就感。跟lua一样,复杂的数据类型我只支持了table,这个table即可以做array也可以做hash_map用。一般用lua的人都会用table去模拟class。lua只对这个做了非常有限的扩展:在lua的文档中,我们可以看到

functiont.a.b.c:f(...)...end可以等同于t.a.b.c.f=function(self,...)...end

就我用lua的经验,这个转换用途不是特别大,只是少写个self而已。

这次我自己设计脚本语言,针对脚本支持OO的问题,特别做了些改进。

的确在大会的第2天我去听了这个演讲,老实说,其内容我觉得不符合我的预期,所以半途我跑到隔壁去听荣耀讲模板元编程了。

当时讲师前半段一直在讲SEH,和C++关系不大。我本以为会讲C++异常的实现的,我个人以前研究过一些,很有兴趣听听人家的理解,结果没有听到。据说后来那个会场最终吵了起来,很遗憾没有领略那个盛况:)

为什么有COM?我的理解是,MS要给对象的二进制表达规范一种标准,好做到二进制模块的复用。到COM的诞生日为止,C++是最适合实现模块对象化的语言,直到现在C++依旧是可以用面向对象的方式直接生成的本地码的最佳工具。可惜C++的实现方案并没有标准,比如对象中数据的布局规则,函数调用的参数传递方式,返回值的处理方式,多继承的实现,等等。

THE END
1.8个数据库设计典型实例PowerBuilder进行信息系统开发的基本知识。下面将通过一个个实例来说明如何利用 PowerBuilder作为数据库前端开发工具,开发出具有使用价值的管理信息系统。 人事管理系统实例是本书的第一个例子。因此对于实例开发过程中所涉及到的一些知 识会有重点讲述。 随着计算机技术的飞速发展,计算机在企业管理中应用的普及,利用计算机...http://www.360doc.com/document/14/0214/20/15804455_352536542.shtml
1.一个简单数据库设计例子一个简单数据库设计例子 一个曾经做过的简单的管理系统中数据库设计的例子,包括设计表、ER图、建模、脚本. +++++++++++++++++++++++++++++++++++++++++++++++ 项目信息 Project Name:Book Manager System DB:MySQL5.5 DB Name:db_library Tables...https://blog.csdn.net/Jerry_1126/article/details/44337973
2.sqlserver架构设计mob649e8162842c的技术博客在开始设计之前,了解一些基本要素是非常重要的。这些要素包括: 表:数据存储的基本单元。 字段:表中的列,每个字段都有数据类型。 索引:用于提高查询性能。 关系:表与表之间的联系。 约束:用于限制数据的输入(如主键、外键、唯一约束等)。 三、设计一个简单的数据库 ...https://blog.51cto.com/u_16175492/12561233
3.AlphaFold3来了!全面预测蛋白质与所有生命分子相互作用及结构...(d) VMD:一个分子可视化程序,用于使用 3D 图形和内置脚本显示、动 态化和分析大型生物分子系统。 1.4 蛋白质设计的常用评估指标:NSR、RMSD、GDT、能量评分函数、可 溶性、与靶标之间的结合强度和特异性 3. 蛋白质数据库介绍 1.1 一级蛋白质序列数据库:UniProtKB ...https://cloud.tencent.com/developer/article/2437597
4.MySQL数据库实验实现简单数据库应用系统设计Mysql这篇文章主要介绍了MySQL数据库实验实现简单数据库应用系统设计,文章通过理解并能运用数据库设计的常见步骤来设计满足给定需求的概念模和关系数据模型展开详情,需要的朋友可以参考一下+ 目录 GPT4.0+Midjourney绘画+国内大模型 会员永久免费使用!【 如果你想靠AI翻身,你先需要一个靠谱的工具!】 观前提示:本篇内容为...https://www.jb51.net/article/252268.htm
5.蓝蓝高频面试之数据库系列第一期数据库基础20题前言[toc] 大家好,我是蓝蓝。数据库在国企,银行,研究所的面试过程中,是占比非常大的,也是及其可能被问的知识点,我将其分为四个部分,分为基础理论篇,事务篇,优化篇,大概一共100个题目,最后一部分是最高频的题目,大家加油哈,星球打卡时间为15天,完成打卡一定有https://m.nowcoder.com/discuss/353158849412669440
6.全国计算机二级考试哪个最简单全国计算机二级考试是全国计算机等级考试简称NCRE,是四个等级中的一个等级。包含语言程序设计,包括C、C++、Java、Visual Basic、WEB程序设计;数据库程序设计(包括VisualFoxPro、Access、MySql);MS office高级应用包括Word、EXCEL、PPT办公软件高级应用。计算机二级考试哪个最简单?下面百分网小编带大家一起来看看详细内容,希望...https://www.oh100.com/kaoshi/ncre2/tiku/482381.html
7.软件工程导论作业控制类:是用于对特定于一个或一些用例的控制行为建模的类。 实体类:是用来对必须存储的信息及关联行为建模的类。 5.6 按照设计模型的不同层次和功能,设计元素可以分哪些方面? 答:(1)体系结构元素;(2)构件级元素;(3)接口/界面元素:用户界面、构件接口、系统接口;(4)数据元素:数据库设计、数据结构设计;(5)部署...https://www.unjs.com/zuixinxiaoxi/ziliao/20170805000008_1416273.html
8.java毕业实习报告(精选11篇)在三个月里,所学知识的确有很多,java基础,数据库操作(oracle,mysql),SSH框架(hibernate,struts,spring),网页设计jsp技术等,总之学到了很多曾经陌生的技术。受益匪浅。 一、实习计划 7月10日:简单地了解公司的基本情况,进一步学习了java的基本知识。7月11日—7月13日:学习java相关的编程环境和运行环境的材料,准备...https://www.fwsir.com/Article/html/Article_20141119170516_283897.html
9.软件工程—精选习题集(含参考答案)总复习60道简答题因此,工程网络图是系统分析和系统设计的强有力的工具。19、动态联编答:动态联编指应用系统在运行过程中,当需要执行一个特定服务的时候,选择(或联编)实现该服务的适当算法的能力。20、系统流程图答:一个概括地描绘物理系统的传统工具,表达了数据在系统各部件之间流动的情况。https://www.jianshu.com/p/6875e17271d0
10.案例数据库设计9篇(全文)校企合作的此类实训项目中,进行数据库设计与实现所用知识、技术,基础来自于校内所学,但又远高于校内简单的数据库理论知识,是学生按软件工程流程做项目开发最重要的一个环节,数据库设计的好坏,直接影响后期软件开发和维护。 摘要:该文介绍了如何使用UML进行数据库设计。首先建立静态模型,然后根据映射策略将模型映射为数据...https://www.99xueshu.com/w/ikey3pf3ms57.html
11.什么是BI?企业数字化的规划和落地第三层,数据源层- 即BI的数据层,各个业务系统底层数据库的数据通过 ETL 的方式抽取到BI的数据仓库中完成 ETL 过程,建模分析等等,最终支撑到前端的可视化分析展现。 02、BI在企业IT信息化中的位置 这一点是所有企业如果规划要上BI项目的时候必须弄明白的:BI在IT信息化中到底处于一个什么样的位置?弄清楚定位是信息...https://maimai.cn/article/detail?fid=1778763808&efid=IIU4kC9omHSMNDd9brumRg
12.ASP.NETCore适用于.NET的开源Web框架.NET 是一个开发人员平台,由工具、编程语言、库组成,用于构建许多不同类型的应用程序。 ASP.NET Core 通过专门用于生成 web 应用的工具和库扩展了.NET 开发人员平台。 更深入发掘: 什么是 ASP.NET Core? 了解ASP.NET Core 通过我们的教程、视频课程和文档,了解 ASP.NET Core 提供的所有功能。 https://asp.net/
13.豆瓣967450 个成员 你来替我吃/用 27984 个成员 下班新生活计划 109315 个成员 再见爱人讨论组 2557 个成员 博士互助组---今天你毕业了吗 96234 个成员 法盲互助协会 149802 个成员 我又买鲜花啦 126005 个成员 人间情侣观察 203772 个成员 电影票房·资料库 49664...https://www.douban.com/
14.保障性理论基础在实施保障性分析的过程中,将收集的大量有关保障的数据与保障性分析记录(logistics support analysis record,LSAR),以一定的格式储存到电子计算机内,建立一个包括可靠性、维修性、测试性及各综合技术保障要素等信息在内的独立的保障性分析记录数据库,并在分析过程中不断地更新数据。该数据库用于装备综合技术保障和保障...https://mp.ofweek.com/im/a645693221836
15.积分商城数据库设计:构建一个完美的线上积分兑换平台(积分商城...随着互联网的发展,越来越多的企业开始在线上销售产品和服务。与此同时,也出现了越来越多的积分兑换平台,为消费者提供了更加便捷的积分兑换服务。然而,在构建一个完美的线上积分兑换平台的过程中,数据库设计是非常关键的。 本文将从以下三个方面来介绍积分商城数据库的设计: ...https://www.idc.net/help/150689/