#
原文地址:http://codemacro.com/2012/07/18/start-to-write-tips/
回首上篇博客的發(fā)表時間,又2個月時間過去了。在我博客生涯的過去兩三年里,總會有好幾個月的時間沒有編寫任何文章。我覺得這真是一個不好的習(xí)慣。這個情況的產(chǎn)生,有很多原因。例如自己太懶、工作偶爾忙、自己偶爾處于混沌時期、自己偶爾懷疑人生,如是種種。但最大的原因還是,不敢寫。
在剛出來工作那會,作為一個懵懂的青年,接觸個什么新技術(shù)都內(nèi)心激動驕傲,然后就特別有動力將所學(xué)記錄下來,注意下言辭還能折騰個像那么回事的教程出來。后來慢慢地,我就覺得,這些東西太膚淺。先別說教人用個什么IDE,配置個什么數(shù)據(jù)庫,就算你是學(xué)個新語言,好好研究下TCP,甚至還能折騰個IOCP框架,這些都還是他媽的特膚淺。你說任何一個有那么點經(jīng)驗和學(xué)習(xí)能力的程序員,誰花點時間整不出來這些?誰他媽要你在這里裝逼賣萌,甚至貽笑大方。除此之外,我個人也覺得無聊了。
另一方面我覺得寫博客還有個好處就是幫助自己記錄技術(shù),以便將來萬一又需要曾經(jīng)學(xué)習(xí)過的技術(shù)了,回頭溫習(xí)一下就好。但是后來慢慢地我又覺得,這也是沒必要的事情。因為反正需要這個技術(shù)的時候,也花不了多少時間。
基于這些亂七八糟的原因,我雖然經(jīng)常打開自己的博客,看看有沒人評論啊,留言啊,但發(fā)表博客的頻率始終上不去。
后來呢,在google reader上斷斷續(xù)續(xù)也看了些別的程序員的故事。例如有傻逼堅持1年每天一篇博客,后來竟然寫了本書;例如有傻逼堅持每天翻譯一篇英文文章。我琢磨著這些人該有多么大的毅力啊,就算是翻譯文章,這從找文章篩選文章到最好發(fā)表出來這尼瑪又該睡覺了啊親。心生佩服之余,我覺得自己應(yīng)該向這些傻逼們學(xué)習(xí)。作為一個已經(jīng)沒有那么多青年時光的青年,試想以后每天下班回家?guī)薜娜兆樱?jīng)竟碌碌無為地磨過每一個工作日耍過每一個工作日晚上,這是件比帶娃更悲劇的事情。
所以,我也決定堅持干一件雖一日不用一次但也望每周那么幾次的事情。我決定在博客上記錄一些編程方面的技巧(tips),集中于某個小問題的解決、某個小功能的實現(xiàn)。這些技巧相比前文說的,就更膚淺了,膚淺到你一google出來的結(jié)果你都嚇一跳的程度。但是我依然覺得這是有用的,就像我用rails做網(wǎng)站,每一個小功能我都得google一遍,然后積累于心,然后一段時間后忘掉。為了不忘掉,為了查閱起來簡單,我決定記錄下來。但是僅靠我自己的經(jīng)驗,是肯定無法做到頻繁地更新的,所以,我決定上stackoverflow上隨機找些問題/答案翻譯出來。stackoverflow非常適合滿足這種需求,我發(fā)現(xiàn)我google某個rails技巧時,基本是從stackoverflow上獲取下來的。
這樣,我的博客http://codemacro.com的rss輸出可能會繁雜點,這對于某些人而言估計會起到惡心的效果。而我自己的博客可能也會變得不那么像個人博客。我也想過單獨做個網(wǎng)站出來,但仔細(xì)想想還是制止自己少瞎折騰了。如有建議歡迎批評。
好,就這樣,沒了。
原文鏈接:http://codemacro.com/2012/05/10/tolua-api/
我們使用tolua++手工綁定c/c++接口到lua中,在綁定的接口實現(xiàn)里,就需要取出傳入的參數(shù)。tolua++中提供了一系列tolua_toxxx函數(shù),例如:
lua_Number tolua_tonumber(lua_State *L, int narg, lua_Number def)
const char *tolua_tostring(lua_State *L, int narg, const char *def)
這些函數(shù)都有一個def參數(shù)。乍一看,這些函數(shù)使用起來很簡單。傳入lua_State,傳入?yún)?shù)在棧中的位置,然后再傳一個失敗后返回的默認(rèn)值。
我重點要說的是這里這個失敗,按正常程序員的理解,針對lua而言,什么情況下算失敗呢?lua語言里函數(shù)參數(shù)支持不傳,此時實參為nil,將nil轉(zhuǎn)換為一個c類型必然失敗;參數(shù)類型不正確算不算失敗?你傳一個user data,c里按數(shù)字來取,這也算失敗。
這么簡單的API還需要多糾結(jié)什么呢?然后我們浩浩蕩蕩地寫了上百個接口,什么tolua_tostring/tolua_tonumber的使用少說也有500了吧?
然后有一天,服務(wù)器宕機了,空指針:
/* 失敗返回"",還能省空指針的判斷 */
const char *name = tolua_tostring(L, 1, "");
if (name[0] == '\0') { /* 空串總得判斷吧 */
...
}
跟蹤后發(fā)現(xiàn),腳本里傳入的是nil,這里的name取出來是NULL,而不是”“(的地址)。然后吐槽了一下這個API,辛苦地修改了所有類似代碼,增加對空指針的判斷。我沒有多想。
故事繼續(xù),有一天服務(wù)器雖然沒宕機,但功能不正常了:
float angle = (float) tolua_tonumber(L, 1, 2 * PI);
...
這個意思是,這個函數(shù)的參數(shù)1默認(rèn)是2*PI,什么是默認(rèn)?lua里某函數(shù)參數(shù)不傳,或傳nil就是使用默認(rèn)。因為不傳的話,這個實參本身就是nil。但,tolua_tonumber的行為不是這樣的,它的實現(xiàn)真是偷懶:
TOLUA_API lua_Number tolua_tonumber (lua_State* L, int narg, lua_Number def)
{
return lua_gettop(L)<abs(narg) ? def : lua_tonumber(L,narg);
}
TOLUA_API const char* tolua_tostring (lua_State* L, int narg, const char* def)
{
return lua_gettop(L)<abs(narg) ? def : lua_tostring(L,narg);
}
意思是,只有當(dāng)你不傳的時候,它才返回默認(rèn)值,否則就交給lua的API來管,而lua這些API是不支持應(yīng)用層的默認(rèn)參數(shù)的,對于lua_tonumber錯誤時就返回0,lua_tostring錯誤時就返回NULL。
這種其行為和其帶來的common sense不一致的API設(shè)計,實在讓人蛋疼。什么是common sense呢?就像一個UI庫里的按鈕,我們都知道有click事件,hover事件,UI庫的文檔甚至都不需要解釋什么是click什么是hover,因為大家看到這個東西,就有了共識,無需廢話,這就是common sense。就像tolua的這些API,非常普通,大家一看都期待在意外情況下你能返回def值。但它竟然不是。實在不行,你可以模仿lua的check系列函數(shù)的實現(xiàn)嘛:
LUALIB_API lua_Number luaL_checknumber (lua_State *L, int narg) {
lua_Number d = lua_tonumber(L, narg);
if (d == 0 && !lua_isnumber(L, narg)) /* avoid extra test when d is not 0 */
tag_error(L, narg, LUA_TNUMBER);
return d;
}
即,根本不用去檢查棧問題,直接在lua_tonumber之后再做包裝檢查。何況,lua需要你去檢查棧嗎?當(dāng)你訪問了棧外的元素時,lua會自動返回一個全局常量luaO_nilobject:
static TValue *index2adr(lua_State *L, int idx) {
...
if (o >= L->top) return cast(TValue*, luaO_nilobject);
}
另,程序悲劇也來源于臆想。
原文鏈接:http://codemacro.com/2012/04/25/game-server-info-2/
上一篇談了一些關(guān)鍵技術(shù)的實現(xiàn)方案。本篇描述一些遇到的問題。
一
在策劃制作完了幾個職業(yè)后(主要是技能制作),大概去年年底公司內(nèi)部進行了一次混戰(zhàn)測試。30個角色在一個場景進行混戰(zhàn),測試結(jié)果從技術(shù)上來說非常不理想。首先是客戶端和服務(wù)器都巨卡無比。服務(wù)器CPU一直是滿負(fù)載狀態(tài)。而客戶端又頻繁宕機。
我們關(guān)注的主要問題,是服務(wù)器CPU滿負(fù)載問題。最開始,我通過日志初步定位為網(wǎng)絡(luò)模塊問題,因為邏輯線程表現(xiàn)不是那么差。然后考慮到技能過程中的特效、動作都是通過服務(wù)器消息驅(qū)動,并且本身特效和動作就比一般網(wǎng)游復(fù)雜,通過逐一屏蔽這一部分功能,最終確認(rèn)確為網(wǎng)絡(luò)模塊導(dǎo)致。然后團隊決定從兩方面努力:重寫網(wǎng)絡(luò)模塊,改善性能;改善技能實現(xiàn)機制,將表現(xiàn)類邏輯移到客戶端。
至于網(wǎng)絡(luò)模塊,在后來才發(fā)現(xiàn),雖然網(wǎng)絡(luò)流量過高,但導(dǎo)致網(wǎng)絡(luò)線程CPU滿的原因竟然是網(wǎng)絡(luò)模塊自身的流量限制導(dǎo)致。而技能實現(xiàn)機制的改善,考慮到改動的成本,最終使用了一種RPC機制,讓服務(wù)器腳本可以調(diào)用客戶端腳本,并且支持傳入復(fù)雜參數(shù)。然后策劃通過一些關(guān)鍵數(shù)據(jù)在客戶端計算出特效、動作之類。
此外,程序?qū)⒏嗟募寄軐傩詮V播給客戶端,一個客戶端上保存了周圍角色的技能數(shù)據(jù),從而可以進行更多的客戶端邏輯。這一塊具體的修改當(dāng)然還是策劃在做(我們的腳本策劃基本就是半個程序員)。后經(jīng)測試,效果改善顯著。
二
在策劃制作了一個PVP競技副本后,服務(wù)器在10V10測試過程中又表現(xiàn)出CPU負(fù)載較高的情況。這個問題到目前為止依然存在,只不過情況略微不同。
首先是觸發(fā)器生命周期的問題。觸發(fā)器自身包含最大觸發(fā)次數(shù)、存留時間等需求,即當(dāng)觸發(fā)一定次數(shù),或超過存留時間后,需要由程序自動刪除;另一方面,觸發(fā)器可以是定時器類型,而定時器也決定了觸發(fā)器的生命周期。這一塊代碼寫的非常糟糕,大概就是管理職責(zé)劃分不清,導(dǎo)致出現(xiàn)對象自己刪除自己,而刪除后還在依賴自己做邏輯。
但這樣的邏輯,最多就是導(dǎo)致野指針的出現(xiàn)。不過,這種混亂的代碼,也更容易導(dǎo)致BUG。例如,在某種情況下觸發(fā)器得不到自動刪除了。但這個BUG并不是直接暴露的,直接暴露的,是CPU滿了。我們的怪物AI在腳本中是通過定時器類觸發(fā)器驅(qū)動的,每次AI幀完了后就注冊一個觸發(fā)器,以驅(qū)動下一次AI幀。由于這個BUG導(dǎo)致觸發(fā)器沒有被刪除,從而導(dǎo)致服務(wù)器上觸發(fā)器的數(shù)量急劇增加。但,這也就導(dǎo)致內(nèi)存增長吧?
另一個巧合的原因在于,在當(dāng)時的版本中,觸發(fā)器是保存一個表里的,即定時器類觸發(fā)器、屬性類觸發(fā)器、移動類觸發(fā)器等都在一個表里。每次任意觸發(fā)器事件發(fā)生時,例如屬性改變,都會遍歷這個表,檢查其是否觸發(fā)。
基于以上原因,悲劇就發(fā)生了。在這個怪物的AI腳本里,有行代碼設(shè)置了怪物的屬性。這會導(dǎo)致程序遍歷該怪物的所有觸發(fā)器。而這個怪物的觸發(fā)器數(shù)量一直在增長。然后就出現(xiàn)了在很多游戲幀里出現(xiàn)過長的遍歷操作,CPU就上去了。
找到這個問題了幾乎花了我一天的時間。因為腳本代碼不是我寫的,觸發(fā)器的最初版本也不是我寫的。通過逐一排除可能的代碼,最終竟然發(fā)現(xiàn)是一行毫不起眼的屬性改變導(dǎo)致。這個問題的查找流程,反映了將大量邏輯放在腳本中的不便之處:查起問題來實在吃力不討好。
修復(fù)了這個BUG后,我又對觸發(fā)器管理做了簡單的優(yōu)化。將觸發(fā)器列表改成二級表,將觸發(fā)器按照類型保存成幾個列表。每次觸發(fā)事件時,找出對應(yīng)類型的表遍歷。
改進
除了修改觸發(fā)器的維護數(shù)據(jù)結(jié)構(gòu)外,程序還實現(xiàn)了一套性能統(tǒng)計機制,大概就是統(tǒng)計某個函數(shù)在一段時間內(nèi)的執(zhí)行時間情況。最初這套機制僅用于程序,但考慮到腳本代碼在整個項目中的比例,又決定將其應(yīng)用到腳本中。
這個統(tǒng)計需要在函數(shù)進入退出時做一些事情,C++中可以通過類對象的構(gòu)建和析構(gòu)完成,但lua中沒有類似機制。最初,我使用了lua的調(diào)試庫來捕獲函數(shù)進入/退出事件,但后來又害怕這種方式本身存在效率消耗,就取消了。我們使用lua的方式,不僅僅是全局函數(shù),還包括函數(shù)對象。而函數(shù)對象是沒有名字標(biāo)示的,這對于日志記錄不是什么好事。為了解決這個問題,我只好對部分功能做了封裝,并讓策劃顯示填入函數(shù)對于的字符串標(biāo)示。
除此之外,因為觸發(fā)器是一種重要的敏感資源,我又加入了一個專門的觸發(fā)器統(tǒng)計模塊,分別統(tǒng)計觸發(fā)器的類型數(shù)量、游戲?qū)ο髶碛械挠|發(fā)器數(shù)量等。
END
到目前為止,導(dǎo)致服務(wù)器CPU負(fù)載過高,一般都是由BUG導(dǎo)致。這些BUG通常會造成一個過長的列表,然后有針對這個列表的遍歷操作,從而導(dǎo)致CPU負(fù)載過高。更重要的,我們使用了這么多的腳本去開發(fā)這個游戲,如何找到一個更有效合理的監(jiān)測方法,如何讓程序框架更穩(wěn)定,則是接下來更困難而又必須去面對的事情。
原文鏈接:http://codemacro.com/2012/04/23/game-server-info-1/
我們的邏輯服務(wù)器(Game Server,以下簡稱GS)主要邏輯大概是從去年夏天開始寫的。因為很多基礎(chǔ)模塊,包括整體結(jié)構(gòu)沿用了上個項目的代碼,所以算不上從頭開始做。轉(zhuǎn)眼又快一年,我覺得回頭總結(jié)下對于經(jīng)驗的積累太有必要。
整體架構(gòu)
GS的架構(gòu)很大程度取決于游戲的功能需求,當(dāng)然更受限于上個項目的基礎(chǔ)架構(gòu)。基礎(chǔ)架構(gòu)包括場景、對象的關(guān)系管理,消息廣播等。
需求
這一回,程序員其實已經(jīng)不需要太過關(guān)心需求。因為我們決定大量使用腳本。到目前為止整個項目主要還是集中在技能開發(fā)上。而這個使用腳本的度,就是技能全部由策劃使用腳本制作,程序員不會去編寫某個具體技能,也不會提供某種配置方式去讓策劃通過配置來開發(fā)技能。這真是一個好消息,不管對于程序員而言,還是對于策劃而言。但后來,我覺得對于這一點還是帶來了很多問題。
實現(xiàn)
基于以上需求,程序員所做的就是開發(fā)框架,制定功能實現(xiàn)方案。腳本為了與整個游戲框架交互,我們制定了“觸發(fā)器“這個概念,大概就是一種事件系統(tǒng)。
這個觸發(fā)器系統(tǒng),簡單來說,就是提供一種“關(guān)心“、”通知“的交互方式,也就是一般意義上的事件機制。例如,腳本中告訴程序它關(guān)心某個對象的移動,那么當(dāng)程序中該對象產(chǎn)生移動時,就通知腳本。腳本中可以關(guān)心很多東西,包括對象屬性,其關(guān)心的方式包括屬性值改變、變大、變小,各種變化形式;對象開始移動,移動停止;對象碰撞,這個會單獨談?wù)劊欢〞r器等。
除了觸發(fā)器系統(tǒng)外,還有個較大的系統(tǒng)是游戲?qū)ο蟮膶傩韵到y(tǒng)。對象的屬性必然是游戲邏輯中策劃最關(guān)心最容易改動的模塊。既然我們程序的大方向是盡可能地不關(guān)心策劃需求,所以對象屬性在設(shè)計上就不可能去編寫某個具體屬性,更不會編寫這個屬性相關(guān)的邏輯功能。簡單來說,程序為每個對象維護一個key-value表,也就是屬性名、屬性值表。該表的內(nèi)容由腳本填入,腳本享有存取權(quán)限。然后腳本中就可以圍繞某個屬性來編寫功能,而程序僅起存儲作用。
第三,怪物AI模塊。AI模塊的設(shè)計在開發(fā)周期上靠后。同樣,程序不會去編寫某類AI的實現(xiàn)。程序提供了另一種簡單的事件系統(tǒng),這個系統(tǒng)其實就是一個調(diào)用腳本的方案。當(dāng)關(guān)于某個怪物發(fā)生了某個事件時,程序調(diào)用腳本,傳入事件類型和事件參數(shù)。這個事件分為兩類:程序類和腳本類。腳本類程序不需關(guān)心,僅提供事件觸發(fā)API。程序類事件非常有限:怪物創(chuàng)建、出生、刪除。
除了以上三塊之外,還有很多零散的腳本交互。例如游戲?qū)ο髮傩猿跏蓟巧M入游戲,角色進入場景等。這些都無關(guān)痛癢。
接下來談一些關(guān)鍵模塊的實現(xiàn)。
定時器
整個GS的很多邏輯模塊都基于這個定時器來實現(xiàn)。這個定時器接收邏輯模塊的注冊,在主循環(huán)中傳入系統(tǒng)時間,定時器模塊檢查哪些定時器實例超時,然后觸發(fā)調(diào)用之。這個主循環(huán)以每幀5ms的速率運行,也即幀率1000/5。
這個定時器是基于操作系統(tǒng)的時間。隨著幀率的不同,它在觸發(fā)邏輯功能時,就必然不精確。游戲客戶端(包括單機游戲)在幀率這塊的實現(xiàn)上,一般邏輯功能的計算都會考慮到一個dt(也就是一幀的時間差),例如移動更新,一般都是x = last_x + speed * dt。但,我們這里并沒有這樣做。我們的幾乎所有邏輯功能,都沒有考慮這個時間差。
例如,我們的移動模塊注冊了一個固定時間值的定時器,假設(shè)是200ms。理想情況下,定時器模塊每200ms回調(diào)移動模塊更新坐標(biāo)。但現(xiàn)實情況肯定是大于200ms的更新頻率,悲劇的是,移動模塊每次更新坐標(biāo)都更新一個固定偏移。這顯然是不夠精確的。
更悲劇的是,定時器的實現(xiàn)中,還可能出現(xiàn)跳過一些更新幀。例如,理論情況下,定時器會在系統(tǒng)時間點t1/t2/t3/t4分別回調(diào)某個邏輯模塊。某一幀里,定時器大概在t1回調(diào)了某邏輯模塊,而當(dāng)該幀耗時嚴(yán)重時,下一幀定時器模塊在計算時,其時間值為t,而t大于t4,此時定時器模塊跳過t2/t3。相當(dāng)于該邏輯模塊少了2次更新。這對于移動模塊而言,相當(dāng)于某個對象本來在1秒的時間里該走5格,但實際情況卻走了1格。
當(dāng)然,當(dāng)游戲幀率無法保證時,邏輯模塊運行不理想也是情有可原的。但,不理想并不包含BUG。而我覺得,這里面是可能出現(xiàn)BUG的。如何改善這塊,目前為止我也沒什么方案。
移動
有很多更上層的模塊依賴移動。我們的移動采用了一種分別模擬的實現(xiàn)。客戶端將復(fù)雜的移動路徑拆分為一條一條的線段,然后每個線段請求服務(wù)器移動。然后服務(wù)器上使用定時器來模擬在該線段上的移動。因為服務(wù)器上的阻擋是二維格子,這樣服務(wù)器的模擬也很簡單。當(dāng)然,這個模塊在具體實現(xiàn)上復(fù)雜很多,這里不細(xì)談。
碰撞檢測
我們的技能要求有碰撞檢測,這主要包括對象與對象之間的碰撞。在最早的實現(xiàn)中,當(dāng)腳本關(guān)心某個對象的碰撞情況時,程序就為該對象注冊定時器,然后周期觸發(fā)檢測與周圍對象的距離關(guān)系,這個周期低于100ms。這個實現(xiàn)很簡單,維護起來也就很簡單。但它是有問題的。因為它基于了一個不精確的定時器,和一個不精確的移動模塊。
首先,這個檢測是基于對象的當(dāng)前坐標(biāo)。前面分析過在幀率掉到移動更新幀都掉幀的情況下,服務(wù)器的對象坐標(biāo)和理論情況差距會很大,而客戶端基本上是接近正確情況的,這個時候做的距離檢測,就不可能正確。另一方面,就算移動精確了,這個碰撞檢測還是會帶來BUG。例如現(xiàn)在檢測到了碰撞,觸發(fā)了腳本,腳本中注冊了關(guān)心離開的事件。但不幸的是,在這個定時器開始檢測前,這兩個對象已經(jīng)經(jīng)歷了碰撞、離開、再碰撞的過程,而定時器開始檢測的時候,因為它基于了當(dāng)前的對象坐標(biāo),它依然看到的是兩個對象處于碰撞狀態(tài)。
最開始,我們直覺這樣的實現(xiàn)是費時的,是不精確的。然后有了第二種實現(xiàn)。這個實現(xiàn)基于了移動的實現(xiàn)。因為對象的移動是基于直線的(服務(wù)器上)。我們就在對象開始移動時,根據(jù)移動方向、速度預(yù)測兩個對象會在未來的某個時間點發(fā)生碰撞。當(dāng)然,對于頻繁的小距離移動而言,這個預(yù)測從直覺上來說也是費時的。然后實現(xiàn)代碼寫了出來,一看,挺復(fù)雜,維護難度不小。如果效果好這個維護成本也就算了,但是,它依然是不精確的。因為,它也依賴了這個定時器。
例如,在某個對象開始移動時,我們預(yù)測到在200ms會與對象B發(fā)生碰撞。然后注冊了一個200ms的定時器。但定時器不會精確地在未來200ms觸發(fā),隨著幀率的下降,400ms觸發(fā)都有可能。即便不考慮幀率下降的情況,它還是有問題。前面說過,我們游戲幀保證每幀至少5ms,本來這是一個限幀手段,目的當(dāng)然是避免busy-loop。這導(dǎo)致定時器最多出現(xiàn)5ms的延遲。如果策劃使用這個碰撞檢測去做飛行道具的實現(xiàn),例如一個快速飛出去的火球,當(dāng)這個飛行速度很快的時候,這5ms相對于這個預(yù)測碰撞時間就不再是個小數(shù)目。真悲劇。
技能
雖然具體的技能不是程序?qū)懙模绨褞缀跛芯唧w邏輯交給策劃寫帶來的悲劇一樣:這事不是你干的,但你得負(fù)責(zé)它的性能。所以有必要談?wù)劶寄艿膶崿F(xiàn)。
技能的實現(xiàn)里,只有一個技能使用入口,程序只需要在客戶端發(fā)出使用技能的消息時,調(diào)用這個入口腳本函數(shù)。然后腳本中會通過注冊一些觸發(fā)器來驅(qū)動整個技能運作。程序員一直希望策劃能把技能用一個統(tǒng)一的、具體的框架統(tǒng)一起來,所謂的變動都是基于這個框架來變的。但策劃也一直堅持,他們心目中的技能是無法統(tǒng)一的。
我們的技能確實很復(fù)雜。一個技能的整個過程中,服務(wù)器可能會和客戶端發(fā)生多次消息交互。在最初的實現(xiàn)中,服務(wù)器甚至?xí)刂瓶蛻舳说募寄芴匦А⑨尫艅幼鞯雀鞣N細(xì)節(jié);甚至于服務(wù)器會在這個過程中依賴客戶端的若干次輸入。
下一篇我將談?wù)勔恍┯龅降膯栴}。
原文鏈接http://codemacro.com/2012/04/20/blog-on-github/
最開始知道Github Page,是通過codertrace上的某些注冊用戶,他們的BLOG就建立在Github Page上,并且清一色的干凈整潔(簡陋),這看起來很酷。
Github提供了很多很合coder口味的東西,例如Gist,也包括這里提到的Page。Page并不是特用于建立博客的產(chǎn)品,它僅提供靜態(tài)頁面的顯示。它最酷的地方,是通過Git的方式來讓你管理這些靜態(tài)頁面。通過建立一個repository,并使用markdown語法來編寫文章,然后通過Git來管理這些文章,你就可以自動將其發(fā)布出去。
當(dāng)然,要搭建一個像樣點的博客,使用Github Page還不太方便。這里可以使用Jekyll。Jekyll是一個靜態(tài)網(wǎng)頁生成器,它可以將你的markdown文件自動輸出為對應(yīng)的網(wǎng)頁。而Github Page也支持Jekyll。
為了更方便地搭建博客,我還使用了Jekyll-bootstrap。jekyll-bootstrap其實就是一些模板文件,提供了一些博客所需的特殊功能,例如評論,訪問統(tǒng)計。
基于以上,我就可以像在Github上做項目一樣,編寫markdown文章,然后git push即可。可以使用jekyll --server在本地開啟一個WEB SERVER,然后編寫文章時,可以在本地預(yù)覽。
Github Page還支持custom domain,如你所見,我將我的域名codemacro.com綁定到了Github Page所提供的IP,而不再是我的VPS。你可以通過kevinlynx.github.com或者codemacro.com訪問這個博客。
當(dāng)然實際情況并沒有那么簡單,例如并沒有太多的theme可供選擇,雖然jekyll-bootstrap提供了一些,但還是太少。雖然,你甚至可以fork別人的jekyll博客,使用別人定制的theme,但,這對于一個不想過于折騰的人說,門檻也過高了點。
jekyll-bootstrap使用了twitter的bootstrap css引擎,但我并不懂這個,所以,我也只能定制些基本的頁面樣式。
1年前我編寫了ext-blog,并且在我的VPS上開啟了codemacro.com這個博客。本來,它是一個ext-blog很好的演示例子,但維護這個博客給我?guī)碇T多不便。例如,每次發(fā)布文章我都需要使用更早前用lisp寫的cl-writer,我為什么就不愿意去做更多的包裝來讓cl-writer更好用?這真是一個垃圾軟件,雖然它是我寫的。另一方面,codemacro.com使用的主題,雖然是我抄的,但依然太丑,并且惡心。
更別說那個消耗我VPS所有內(nèi)存的lisp解釋器,以及那惡心的兩位數(shù)字乘法的驗證碼---你能想象別人得有多強烈的留言欲望,才愿意開一個計算器?
說說codertrace.com。我其實寫了篇關(guān)于codertrace.com的總結(jié),但沒有作為博客發(fā)布。做這個事情的結(jié)果,簡單總結(jié)來說就是瞎JB折騰沒有任何結(jié)果。我真的是個苦逼雙子男,我每次做件事情都需要巨大的毅力才能讓自己專注下去。
整個過程中,收到了些網(wǎng)友的郵件,看到了些評論,雖然不多。郵件/評論中有建議的,也有單純的交流的,也有單純鼓勵的。我想說的是,thanks guys。
Anyway, try Github Page, save your VPS money :D.
簡介
因為寫 ext-blog 的原因,慢慢喜歡上github_ 。然后突然有一天產(chǎn)生了一個想法:如果可以把自己的博客_ 和 github主頁 集中到一塊展示給別人,會不會是一種很方便的自我簡介方式?然后我就動手寫了 codertrace.com 。
所以, codertrace.com 這個網(wǎng)站的作用就是用來集中讓程序員炫耀的。它通過RSS抓取,將你的博客,github主頁,或其他有RSS輸出的信息集中到一塊展示給別人。這些信息通常就可以代表一個程序員。
如果你是程序員,也不妨試試。
技術(shù)信息
不知道哪個王八蛋說的,程序員每一年得學(xué)一門新語言。我2010年末接觸了Lisp,然后莫名其妙地寫了 ext-blog ,又莫名其妙地在2011年末接觸了Ruby。因為大學(xué)期間太癡迷C++,我勤奮努力,幾乎通曉這門語言的各種細(xì)節(jié);后來又稍微實踐了下編譯原理。在這若干年間,斷斷續(xù)續(xù)也接觸過其他腳本類語言,我甚至在android上用java寫過幾個 小應(yīng)用 。基于這些積累,我發(fā)現(xiàn)我可以很快上手Ruby,然后再上手Rails,然后就有了 codertrace.com (當(dāng)然還做過一些小的 APP )
所以, codertrace.com 就是一個Ruby on Rails的應(yīng)用。當(dāng)我用這貨來做WEB的時候,我才發(fā)現(xiàn)曾經(jīng)用Lisp寫博客是多么geek。這種感覺就像你在用匯編寫一個GUI程序一樣。我的意思是,ruby/rails的世界里有太多現(xiàn)成的東西,但lisp的世界里沒有。
而且,ruby是一個很爽的語言。我太喜歡它的closure語法,簡潔,不需要加其他關(guān)鍵字就可以構(gòu)造(例如其他語言map(function (item) xxxx end),或者map(lambda (item) xxx ))。但我不喜歡在使用的地方通過yield去調(diào)用---這就像一個hack。我更不喜歡ruby用proc去封裝closure。好吧,這其實是我自我分裂,為什么我要把ruby看成一個函數(shù)式語言?
腳本語言真是太酷了。
服務(wù)器信息
我很窮。不管你信不信,我真的舍不得花1000RMB買個VPS來架設(shè) codertrace.com 。目前, codertrace.com 架設(shè)在 heroku.com ,而且還使用的是免費服務(wù)。免費服務(wù)竟然只有5M數(shù)據(jù)庫。 codertrace.com 后臺為了異步抓取用戶提供的RSS,還使用了一個單獨的進程(delayed_job ruby gem)。這也不是免費的。
但ruby的世界里有太多現(xiàn)成的東西了,甚至有很多現(xiàn)成的庫解決這里的兩個問題:heroku_external_db,這個gem可以讓codertrace使用heroku以外的數(shù)據(jù)庫,然后我就在我的VPS上搭了個mysql,這下流量和網(wǎng)站響應(yīng)速度悲劇了啊,你想你請求個頁面,這個頁面直接涉及到若干條數(shù)據(jù)庫查詢。而這些查詢的請求和回應(yīng)竟然是通過internet網(wǎng)絡(luò)傳輸?shù)摹?/p>
workless,這個gem可以在有異步任務(wù)時,例如codertrace上讀取RSS,就會自動開啟這個worker進程,然后heroku開始計費,當(dāng)沒有任務(wù)時,它又自動關(guān)閉這個進程。雖然省了美元,但再一次讓網(wǎng)站的響應(yīng)速度打了折扣。
為了實現(xiàn)自定義域名,我需要將 codertrace.com 指向 heroku.com 提供的IP。但也許你會同我一樣憤怒,因為它提供的幾個IP都被GFW墻了!所以,目前的實現(xiàn)方案是,我將 codertrace.com 指向了我博客對應(yīng)的VPS,然后在VPS上使用nginx反向代理到 heroku.com 提供的IP。即使如此,我最近甚至發(fā)現(xiàn) codertrace.com 竟然神奇般地會域名解析錯誤,難道godaddy的name server也要被GFW和諧??
故事
作為一個宅男,在工作的若干年中,若干個假期我都用來打游戲,或者寫程序。
所以,當(dāng)這個成為習(xí)慣的時候, codertrace.com ,就順理成章地消費了我今年的春節(jié)假期。我發(fā)現(xiàn)一個人窩在租的小房子里寫代碼是件很爽的事情。在當(dāng)前這個社會環(huán)境下,你可以專注地去干件喜歡的事情,還不用處理各種生活瑣事,真是太爽了。
但為什么我平時得不到這種感覺?因為,我,是一個沒錢的程序員。我和我老婆租在一個標(biāo)間里。在這樣狹小的空間里,多個人就是多幾倍干擾。這太殘酷了。
末了
曾經(jīng)我以為我很牛逼,曾經(jīng)我以為程序員很牛逼。后來我慢慢發(fā)現(xiàn)自己很垃圾。我沒有寫出來過牛逼的程序,大概也沒能力寫。還記得那個程序員的故事嗎?就是有個傻逼也以為程序員很牛逼,但不幸在一家非IT公司的IT部門工作,他的程序員同事的工作就是每周填個excel表格。他后來很絕望,因為他沒有為世界貢獻過任何代碼。后來,這貨丟下一切,坐上去某地的飛機走了。
Author: |
Kevin Lynx |
Date: |
9.29.2011 |
Contact: |
kevinlynx at gmail dot com |
本文描述如何使用Lisp工具集搭建一個完整的個人博客站點。一個搭建好的例子站點可以參看我的個人博客:http://codemacro.com。
要搭建一個獨立博客,需要兩方面的支持。一是博客軟件,二是根據(jù)選擇的博客軟件取得必須的“硬件“。例如我這里使用的是Lisp工具集,就需要一個可以完全控制的服務(wù)器,所以這里我需要一個VPS。當(dāng)然,購買一個合適的域名也是必須的。以下將針對這些內(nèi)容做描述。
獲取VPS及域名
VPS提供商國內(nèi)國外都有很多。我選擇的是 rapidxen ,128M內(nèi)存,1年70來美元,算是國外比較便宜的,速度上還過得去。
購買了VPS后,可以進入后臺管理頁面安裝VPS操作系統(tǒng)。同樣,因為我使用的是Lisp,我選擇安裝了Debian 6.0 squeeze (minimal)64位。實際上我更傾向于32位,因為我的PC系統(tǒng)就是32位,方便測試。安裝系統(tǒng)非常簡單,基本隨意設(shè)置下即可。值得注意的是,除了修改root密碼外,最好修改下ssh端口,具體設(shè)置方法可以另行搜索。此外,因為后面我會使用nginx作為HTTP前端服務(wù)器,為了方便安裝nginx,最好更新下軟件源列表,編輯etc/apt/source.list:
deb http://ftp.us.debian.org/debian squeeze main
deb http://packages.dotdeb.org stable all
deb-src http://packages.dotdeb.org stable all
deb http://php53.dotdeb.org stable all
deb-src http://php53.dotdeb.org stable all
購買VPS最主要的,就是獲取到一個獨立IP,如圖:
然后可以去購買域名。同樣,也有很多域名服務(wù)商。這里我選擇的是 godaddy ,我選擇的域名codemacro.com一年11美元。購買了域名后,就需要將域名和VPS IP關(guān)聯(lián)起來。詳細(xì)設(shè)置也可以另行搜索。這里簡要提下:在成功登入godaddy后,選擇My Account,進入自己的域名,選擇DNS Manager,然后添加域名映射即可,如圖:
通過以上設(shè)置后,你購買的域名就成功指向你購買的VPS地址了。可以通過ping來觀察是否指向成功。
使用Lisp構(gòu)建博客系統(tǒng)
要在VPS上安裝軟件,首先需要SSH上你的VPS,例如:ssh -p 1234 root@codemacro.com。
這里使用的軟件集包括:
實際上,可以完全使用Lisp作為Web服務(wù)器,但我擔(dān)心效率問題(對個人博客而言完全沒這回事),所以使用了nginx作為Web服務(wù)器前端,將hunchentoot放在后面。
安裝nginx
在設(shè)置好debian軟件源后,安裝非常簡單:
apt-get install nginx
安裝完后,因為要將HTTP請求轉(zhuǎn)發(fā)給Lisp服務(wù)器,所以需要修改下配置:
vi /etc/nginx/sites-avaiable/default
將/請求派發(fā)給Lisp服務(wù)器(假設(shè)監(jiān)聽于8000端口):
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
然后可以啟動nginx了:
nginx
這個時候通過瀏覽器訪問,會得到503 bad gateway的錯誤提示,因為hunchentoot還沒開啟。
安裝Lisp相關(guān)軟件
SBCL 同樣可以通過apt直接安裝:
apt-get instal sbcl
裝好SBCL后,就可以進一步安裝 quicklisp 。可以完全遵守quicklisp官方給的安裝方法進行安裝。大概就是先獲取quicklisp.lisp文件,然后在SBCL中載入,根據(jù)提示即可。這里不再贅述。
安裝好quicklisp后,就可以使用它安裝很多Lisp軟件/庫了。quicklisp在安裝一個Lisp庫時,會自動下載并安裝依賴庫,就像apt-get一樣。因為ext-blog并未收入到quicklisp的軟件列表里,所以ext-blog需要手動安裝。首先,在本地(非VPS上)獲取ext-blog源碼:
git clone git://github.com/kevinlynx/klprj.git
上面的git是我個人存東西用的,暫時沒將ext-blog單獨放置。進入到ext-blog目錄。該目錄下有幾個方便的腳本可以用于博客管理。首先將ext-blog打包并上傳到VPS上,例如:
./upload-dist.sh root@codemacro.com 1234 /home/test
該腳本會調(diào)用make-dist.sh將ext-blog全部源碼打包,然后使用scp拷貝該文件及update-blog.sh到VPS指定的目錄里(這里是/home/test),然后ssh上VPS。期間會兩次輸入VPS系統(tǒng)的密碼。然后以下操作將在VPS上完成。
首先進入到剛才拷貝文件的目錄:
cd /home/test
解壓ext-blog.tar.gz:
tar xvf ext-blog.tar.gz
然后ext-blog被解壓到/home/test/dist目錄。進入此目錄運行SBCL:
cd dist
sbcl
ext-blog目錄下dep.lisp會使用quicklisp安裝依賴庫,進入SBCL后,載入該文件即可安裝所有依賴庫,這可能需要一點時間:
(load "dep.lisp")
在沒有其他問題下,可以暫時退出SBCL完成一些其他準(zhǔn)備工作。
ext-blog在最近的版本中加入了驗證碼生成功能,這需要一個pcf字體文件。因為字體文件一般較大,所以upload-dist.sh腳本并沒有將該字體文件打包,所以這里需要手動復(fù)制,同樣在本地的ext-blog目錄下:
scp -P 1234 data/wenquanyi_12ptb.pcf root@codemacro.com:/home/test/dist/data/
另外,因為需要將Lisp解釋器放置在系統(tǒng)后臺執(zhí)行,避免關(guān)掉SSH會話后終止SBCL進程,所以這里需要個工具gnu screen。可以使用apt-get來安裝:
apt-get install screen
然后,一切就OK了。在VPS上可以使用ext-blog目錄下的run-blog.sh來運行這個博客(首先確定VPS上的nginx開啟):
./run-blog.sh
該腳本會使用screen在后臺開啟一個SBCL進程,并自動載入ext-blog,然后在8000端口上開啟HTTP服務(wù)。這個啟動過程可能會使用幾十秒的時間,直接ctrl+z退出screen,這并不終止SBCL。一段時間后便可在瀏覽器里測試。
設(shè)置博客
如果一切正常,此時通過瀏覽器訪問你的站點時,會被重定向到一個博客初始化頁面,如下:
上圖中我是在本機測試的,所以域名是localhost,希望不至于產(chǎn)生誤解。初始化僅需輸入用戶名和密碼即可,以后可通過該用戶名和密碼進入博客后臺管理頁面。完成這一步后,就可以進入博客后臺管理頁面做更多的設(shè)置,例如博客標(biāo)題等。
ext-blog的管理頁面使用了emlog博客系統(tǒng)的CSS及其他資源,因此有同學(xué)覺得管理頁面很面熟就不奇怪了。ext-blog提供在線編輯博客功能,同時也支持簡單的metaweblog API,因此可以使用一些博客客戶端來發(fā)表文章(僅測過我自己寫的博客客戶端cl-writer)。
最后
本文描述較為粗略,主要是很多細(xì)節(jié)我自己也記不清。如有問題可以發(fā)郵件給我。
4月份的時候基于nuclblog寫過一個簡單的博客系統(tǒng),但是因為寫得丑,代碼耦合度高,又有很多硬編碼。當(dāng)然nuclblog本身就寫得不怎么樣,所以6月分的時候就用Lisp寫了新版的ext-blog。支持自定義主題,套個馬甲上去像模像樣。
ext-blog是一個使用Common Lisp編寫的博客系統(tǒng)。基于之前基于nuclblog修改的經(jīng)驗,新的ext-blog最大程度地將博客本身的邏輯與前臺渲染分離開,并且添加了對主題 (theme)的支持。制作新的主題可以隨便找一個WordPress的主題,然后將php代碼翻譯成Lisp代碼即可。
ext-blog底層代碼非常少,其實基本的博客系統(tǒng)功能本來就不多。大部分功能都是在6月初完成。那個時候公司每天加班,下班回去后還寫點Lisp代碼。后來越整越累,就實在沒那完善它的心情,一拖就拖到7月底,功能都還不算完善(至少還得加個rss導(dǎo)出吧?)。
關(guān)于主題開發(fā)
ext-blog主要有幾個頁面派發(fā),對每個頁面都派發(fā)給具體的主題模塊,讓其完成渲染。編寫一個主題本質(zhì)上就是生成html頁面。在Lisp的世界中有很多庫可以生成html。ext-blog的主題也不限制你使用哪一個html生成庫。目前我自己移植的2個WordPress主題,使用的都是google的closure-template的Lisp移植版本,即cl-closure-template。closure-template會從模板產(chǎn)生出 Lisp函數(shù),這一點是比同類庫中的html-template方便一點。當(dāng)然,作為一個模板語言,內(nèi)置判斷、循環(huán)則是必須的。
關(guān)于網(wǎng)絡(luò)框架
世界上很多流行的語言都有流行的Web開發(fā)框架。Lisp方面,我最開始選用的是Weblocks,我甚至用它為公司寫了個簡單的訂餐系統(tǒng)(這讓一個程序員頗有自豪感)。但終究覺得Weblocks太難用,復(fù)雜,但沒有實際功能。我甚至閱讀了它80%的源代碼,但依然獲取不到如何更好使用它的思想。然后恰好我看了些Rails例子,雖然我不懂Ruby語言(依然可以看到很多語言特性有Lisp的影子),但看懂例子還不是大問題。后來我決定自己寫個 Web框架,因為其實我主要需要的就是一個url派發(fā)(route),就像Rails那樣。我甚至為此做了些詳細(xì)設(shè)計,結(jié)果后來不幸發(fā)現(xiàn)Lisp里已經(jīng)有一個類似的框架了,這就是Restas。ext-blog基于Restas。
關(guān)于后臺管理
后臺管理這東西其實可要可不要。就算沒有后臺管理,也可以通過增強RPC來實現(xiàn)。但并不是每個人都是Lisper,相信想了解ext-blog的人很大一部分都是想學(xué)習(xí)Lisp的人。綜合來看,擁有一個后臺管理功能,提供更友好的操作界面,也是非常有必要的。但我確實不擅長做前臺美化的工作。幸運地是我將渲染和邏輯分離開了,后臺管理也算是主題的一種。然后,我抄了emlog博客系統(tǒng)的后臺管理,如前所說,也就是把php代碼(雖然我也不懂php)翻譯成lisp代碼。
關(guān)于開源
ext-blog是完全有理由發(fā)布到common-lisp.net上的,甚至還可以加入到quicklisp的庫列表里。但前提是排除盡可能多的 bug,寫一系列英文文檔,以及最重要的,對其進行長期維護。不幸的是我目前沒有這個時間和精力。所以,只能暫時在這里發(fā)布下了。
要圍觀效果的請移步至我的獨立博客:http://codemacro.com。關(guān)于ext-blog更正式的介紹請移步此篇:http://codemacro.com/view/8。
ps,之前訂閱我獨立博客的TX麻煩更換下rss地址:http://codemacro.com/feed,而博客主頁也最好換成http://codemacro.com。
MMO游戲?qū)ο髮傩栽O(shè)計
Author: |
Kevin Lynx |
Date: |
5.2.2011 |
一般的MMORPG中,游戲?qū)ο笾饕ü治锖屯婕摇_@兩類對象在經(jīng)過游戲性方面的不斷“進化”后,其屬性數(shù)量及與之相關(guān)的邏輯往往會變得很巨大。如何將這一塊做得既不損失效率,又能保證結(jié)構(gòu)的靈活、清晰、可維護?本文將提供一種簡單的結(jié)構(gòu)。
原始結(jié)構(gòu)
最原始的結(jié)構(gòu),極有可能為這樣:
Player: +---------------+
| property-1 |
+---------------+
| property-2 |
+---------------+
| ... |
+---------------+
| operator-1 |
+---------------+
| operator-2 |
+---------------+
| ... |
+---------------+
也就是,一個對象為一個C++類,然后里面直接塞滿了各種屬性名,然后是針對這個屬性的邏輯操作(函數(shù))。其結(jié)果就是Player成為巨類。針對這個情況,一直以來我覺得可以使用一種簡單的方法來拆分這個類。冠以官腔,稱之為Entity-Component-based Desgin。產(chǎn)生這種想法和我的個人技術(shù)積累有一定關(guān)系,見下文。
Policy-based Design
Policy-based Design,基于決策的設(shè)計。這個概念來源于<Modern C++ Design>。雖然這本書講述的是針對C++模板的使用及設(shè)計技巧。但這種思想依然被我潛意識般地用在其他地方。Policy大致來說就是一個小的組件(Component)。它努力不依賴于其他東西,它可能就是個簡單的類,它擁有極少的數(shù)據(jù)結(jié)構(gòu),及針對這些數(shù)據(jù)的極少操作接口。舉例而言,玩家MP的自動回復(fù)功能,就可封裝為一個Policy。將許多Policy組合起來,就可完成一個復(fù)雜的功能。
這種思想還可指導(dǎo)很多程序結(jié)構(gòu)方面的設(shè)計。例如在做功能的接口拆分時,就將每個函數(shù)設(shè)計得足夠小,小到單純地完成一個功能。一個功能的入口函數(shù),就將之前實現(xiàn)的小函數(shù)全部組合起來,然后共同完成功能點。
當(dāng)然,<Modern C++ Design>里的Policy在表現(xiàn)形式上有所不同。但其核心思想相同,主要體現(xiàn)在 組合 特點上。
Entity-Component-based Design
Entity-Component-based Design按照google到的文章,嚴(yán)格來說算是與OOP完全不同的軟件設(shè)計方法。不過在這里它將按照我的意思重新被解釋。
如果說Policy-based Design極大可能地影響著我們平時的細(xì)節(jié)編碼,那么Entity-Component則是直接對游戲?qū)ο蟮慕Y(jié)構(gòu)設(shè)計做直接的說明。 一個游戲?qū)ο缶褪且粋€Entity。 Entity擁有很少的屬性,也許僅包含一個全局標(biāo)示的ID。 一個Component則是Entity的某個行為、或者說某個組成部分。 其實說白了,以玩家為例,一個玩家對象就是一個Entity,而一個MP的自動回復(fù)功能就可被包裝為一個Component。這個Component可能包含若干與該功能相關(guān)的數(shù)據(jù),例如回復(fù)時間間隔,每次的回復(fù)量等。我們往玩家對象這個Entity添加各種Component,也就是給玩家添加各種邏輯功能。
但是,Component之間可能會涉及到交互,玩家對象之外的模塊可能也會與玩家內(nèi)的某個Component交互。子功能點的拆分,不得不涉及到更多的膠水代碼,這也算一種代價。
游戲?qū)ο髮傩栽O(shè)計
這份屬性結(jié)構(gòu)設(shè)計,基本就是參考了上面提到的設(shè)計思想。整個系統(tǒng)有如下組件:
Entity: +-------------------+
| property-table |
+-------------------+
| component-table |
+-------------------+
Property: +-------------------+
| observer-list |
+-------------------+
Component: +--------------------+
| logic-related data |
+--------------------+
| logic-related func |
+--------------------+
意即,所有Entity都包含一個屬性表和組件表。這里的屬性表并非硬編碼的屬性數(shù)據(jù)成員集合,而是一個key-value形式的表。Property包含一個觀察者列表,其實就是一系列回調(diào)函數(shù),但是這些觀察者本質(zhì)上也是組件,后面會提到。Component正如上文描述,僅包含Component本身實現(xiàn)的功能所需要的數(shù)據(jù)和函數(shù)。整個結(jié)構(gòu)大致的代碼如下:
class Entity {
private:
GUID id;
std::map<std::string, IComponent*> components;
std::map<std::string, Property*> properties;
};
class Property {
private:
std::string name;
Value val;
std::vector<IComponent*> observers;
};
class IComponent {
public:
virtual bool Operate (const Args &args) { return false; }
virtual void OnNotify (const Property &property, const Args &args) {}
protected:
std::string name;
Entity *entity;
};
屬性本身是抽象的,這完全是因為我們將屬性統(tǒng)一地放在了一個表里。從而又導(dǎo)致屬性的值也需要繼續(xù)閱讀
多重继承和void*的糗事
Author: | Kevin Lynx |
---|
Date: | 4.30.2011 |
---|
C++为了兼容C,导致了不少语言阴暗面。Bjarne Stroustrup在<D&E>一书里也常为此表现出无奈。另一方面,强制转换也是C++的一大诟病。但是,因为我们的应用环境总是那么“不
纯”,所以也就常常导致各种问题。
本文即描述了一个关于强制转换带来的问题。这个问题几年前我曾遇到过(<多线程下vc2003,vc2005对虚函数表处理的BUG?>),当时没来得及深究。深究C++的某些语法,实在是件辛苦事。所以,这里也不提过于诡异的用法。
问题
考虑下面非常普通的多重继承代码:
class Left {
public:
virtual void ldisplay () {
printf ("Left::ldisplay\n");
}
};
class Right {
public:
virtual void rdisplay () {
printf ("Right::rdisplay\n");
}
};
class Bottom : public Left, public Right {
public:
virtual void ldisplay () {
printf ("Bottom::ldisplay\n");
}
};
这样子的代码在我们的项目中很容易就会出现,例如:
class BaseObject;
class EventListener;
class Player : public BaseObject, public EventListener
别紧张,我当然不会告诉你这样的代码是有安全隐患的。但它们确实在某些时候会出现隐患。在我们的C++项目中,也极有可能会与一些纯C模块打交道。在C语言里,极有肯能出现以
下的代码:
typedef void (*allocator) (void *u);
void set_allocator (allocator alloc, void *u);
之所以使用回调函数,是出于对模块的通用性的考虑。而在调用回调函数时,也通常会预留一个user data的指针,用于让应用层自由地传递数据。
以上关于多重继承和void*的使用中,都属于很常规的用法。但是当它们遇到一起时,事情就悲剧了。考虑下面的代码:
Bottom *bobj = new Bottom(); // we HAVE a bottom object
Right *robj = bobj; // robj point to bobj?
robj->rdisplay(); // display what ?
void *vobj = bobj; // we have a VOID* pointer
robj = (Right*) vobj; // convert it back
robj->rdisplay(); // display what?
这里的输出结果是什么呢?:
Right::rdisplay
Bottom::ldisplay // !!!!
由void*转回来的robj调用rdisplay时,却调用了莫名其妙的Bottom::ldisplay!
多重继承类的内存布局
类对象的内存布局,并不属于C++标准。这里仅以vs2005为例。上面例子中,Bottom类的内存布局大概如下:
+-------------+
| Left_vptr |
+-------------+
| Left data |
+-------------+
| Right_vptr |
+-------------+
| Right data |
+-------------+
| Bottom data |
+-------------+
与单继承不同的是,多重继承的类里,可能会包含多个vptr。当一个Bottom对象被构造好时,其内部的两个vptr也被正确初始化,其指向的vtable分别为:
Left_vptr ---> +---------------------+
| 0: Bottom::ldisplay |
+---------------------+
Right_vptr ---> +---------------------+
| 0: Right::rdisplay |
+---------------------+
转换的内幕
类体系间的转换
隐式转换相比强制转换而言,一定算是优美的代码。考虑如下代码的输出:
Bottom *bobj = new Bottom();
printf ("%p\n", bobj);
Right *robj = bobj;
printf ("%p\n", robj);
其输出结果可能为:
003B5DA0
003B5DA4
结论就是,Right *robj = bobj;时,编译器返回了bobj的一个偏移地址。 从语言角度看,就是这个转换,返回了bobj中Right*的那一部分的起始地址。但编译器并不总是在bobj上加一个偏移,例如:
bobj = NULL;
Right *robj = bobj;
编译器不会傻到给你一个0x00000004的地址,这简直比NULL更无理。
void*转换
编译器当然有理由做上面的偏移转换。那是因为在编译阶段,编译器就知道bobj和Right之间的关系。这个偏移量甚至不需要在运行期间动态计算,或是从某个地方取。如果你看过上面代码对应的汇编指令,直接就是:
add eax, 4 ; 直接加 sizeof(Left),记住,Right在Left之后
void*就没那么幸运了。void*和Bottom没有任何关系,所以:
void *vobj = bobj; // vobj的地址和bobj完全相同
然后当你将vobj转换到一个Right*使用时:
robj = (Right*) vobj; // 没有偏移转换,robj == vobj == bobj
robj->rdisplay();
robj指向的是Bottom的起始地址,天啊,在我们学习C++时,我们可以说Bottom就是一个Left,也是一个Right,所谓的is kind of。但这里的悲剧在于,按照上面的逻辑,我们在使用Right时,其实应该使用Bottom里Right那一部分。 但现在这个转换,却让robj指向了Bottom里Left那一部分。
当调用 robj->rdisplay 时,编译器当然按照Right的内存布局,生成一个虚函数的调用指令,大概就是:
mov vptr, robj->[0] ;; vptr在robj起始地址处
mov eax, vptr[0] ;; rdisplay在vtable中位于第一个
mov ecx, robj
call eax
总而言之, robj->rdisplay 就是使用偏移0处的值作为vptr,然后使用vptr指向的vtable中第一个函数作为调用。
但,robj正指向bobj的起始地址,这个地址是放置Left_vptr的地方。这个过程,使用了Left_ptr,而Left_ptr指向的vtable中,第一个函数是什么呢?:
Left_vptr ---> +---------------------+
| 0: Bottom::ldisplay |
+---------------------+
正是Bottom::ldisplay!到这里,整个问题的原因就被梳理出来了。
;;END;;