混合编程黑科技:跨语言编程问题迎刃而解的3个要点
首先,混合編程是什么鬼?
這個世界上編程語言真不少,光常用就有:C、C++、Java、C#、Objective-C、Javascript、Python、Lua、Swift等等等,遑論一些專業性比較強的DSL了。而且軟件的應用場景也數不勝數:嵌入式設備、后端服務器、桌面程序/GUI、移動端平臺、Web、并行計算……
那么,如果某個場景下光靠一種語言無法滿足業務需求該怎么辦;亦或是某個依賴的庫早已有其他語言編寫的成熟可靠版本,重寫完全劃不來;再者有可能每一個開發者有自己偏好的開發語言,但卻不得不一起協作?
我想,解決這些問題的最好方式,就是采用混合編程,也就是使用不止一種程序設計語言來開發應用程序這種方案。
那么,混合編程背后的原理是什么?
編程語言,以我淺薄的認識來說,大概本質上是對機器語言(1和0)的高度抽象,是基于不同思維方向和設計模式的抽象,所以才會被人為區分成過程式、面向對象、函數式等等不同類別。那么,這種甚至是基于不同思維層面而發明的語言,看似應該老死不相往來,如何才能相互調用呢?
幸好,計算機科學領域中有一個超重要又很實用的概念,那就是“分層”!有一句話怎么說來著?“計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決”。
以上面這張粗糙的圖來說,假設存在語言1和語言2基于重要的分層概念,對于這兩種語言的支持,肯定是從硬件、驅動、操作系統、編譯器+運行時等等一層層疊加上來的,那么,如果想要跨這兩種語言進行調用,要怎么辦呢?
這時候,可以把每一種語言的運行支持比作一棟樓房(原諒我這不甚恰當的類比,但有沒有發現其實二者架構神似?),那么現在想要達到的目標好比就是,把一條消息從一號樓的頂樓送到二號樓的頂樓,這完全可以用我們日常生活中的簡單常識搞定對不對?很顯然,要做的是,確定這兩棟樓之間有沒有天橋連接(一樓也可以被認為是0高度的天橋……),如果一號樓的樓層x與二號樓的樓層y之間存在一條路徑的話,信使就可以首先從一號樓的頂樓下到樓層x,然后通過天橋到達二號樓的樓層y,最終上到二號樓的頂樓就達成了我們的目標!
上述這通看似廢話的介紹暗示了什么呢?不就等同于,任意兩種語言,若是在其運行環境支持的某一層上可以通過某種渠道某種技術可以進行無礙溝通的話,從原理上來說,就實現了二者相互調用的目的嗎(似乎發現了什么了不得的東西……)
然后,來看看混合編程的具體案例
閑話休表,介紹一下本文的主角,在iOS開發中遇到的,混合編程的兩種典型案例——Objective-C與javascript,以及Objective-c與lua。
關于Javascript(后面均簡稱JS)本文不再贅言,主要講一講iOS端有什么方式可以執行JS代碼呢,相信從事iOS開發的諸位都了解UIWebView有如下方法:
(nullable NSString?)stringByEvaluatingJavaScriptFromString:(NSString?)script;?或者WKWebView的
(void)evaluateJavaScript:(NSString?)javaScriptString completionHandler:(void (^?nullable)(nullable id, NSError?__nullable error))completionHandler; `` 都可以來執行一段JS腳本代碼字符串。當然,這是單向的調用,如果想要實現反向的調用怎么辦呢? 為此在iOS 7中正式引入JavaScriptCore框架,可以大幅度簡化Objective-C與JS之間的相互調用過程,幾段簡單的示例代碼如下:
喔!看起來真的很簡單,所有JS相關的變量對象都被封裝成了JSContext和JSValue類型,看起來既清爽又簡約。但這好比是Apple在剛才那兩棟樓的樓頂上各搭建了一個黑黝黝的通道入口,開發者可以把東西扔進去,在另一個樓頂的通道出口就可以輕輕松松收到了,簡直是魔法般的存在!但這種黑盒背后隱藏了大量的細節,而那句話怎么說來著?細節是魔鬼!
這種黑科技背后的實現原理是什么呢
既然不太甘心,那么不妨也自己動手嘗試下如何實現從基礎這一套神奇的通道,在此我選擇的就是Objective-C和Lua這對組合了。首先簡要介紹下主角Lua,它是一種優雅又簡單易學的編程語言,支持自動內存管理、詞法作用域、閉包、迭代器、協程(coroutine)、尾調用等特性,其變量引用規則采用詞法作用域或者說靜態作用域方式,而且數據結構特別簡潔明了(只有table這一種,兼任數組和哈希表二種角色)。
下面簡單的代碼可以幫助大家熟悉一下Lua:
local saySomeThing = function()print("holy shit!")endlocal t = {10, "hello world!"} t["func"] = saySomeThing local b = 100function add(one, another) ? ?return one + anotherendlocal result = add(t[1], b) print(" t[1] + b = ", result)//輸出110t["func"]() //輸出 holy shit!另外,Lua具有很好的可擴展性和可嵌入特性,所以被稱為膠水語言(glue language)。這么贊譽它是因為原始提供了設計良好的C API,可供開發者自行搭建C和Lua世界的通道。
那么,開始動手吧。 首先,假設我們需要把3個C函數導入到Lua去被調用,那么就得到了如下所示代碼。他們似乎看起來有共同之處,那么就是除了方法名之外的函數簽名是完全一樣的,即傳入參數一定是lua_State?類型,返回值一定是static int類型。沒錯,這就是Lua世界給出的一個強制約定,也就是所有滿足如下簽名要求的C函數才有被Lua的運行時接納的資格。當然這種約束也是有目的的:傳入的lua_State?類型變量即是Lua的運行上下文對象,可以從中提取從lua世界傳入的各種參數;而返回的static int類型數值則是代表從C的世界中往Lua的世界中傳入了多少個數據。
然后讓我們把這3個方法導入到Lua的世界中去。
//C 代碼static const struct luaL_reg customLib[] = {{"add", add},{"saySomething", saySomething},{"transformToUpper", transformToUpper} };.....self.state = luaL_newstate();luaL_openlibs(self.state); luaL_register(self.state, "native", customLib);而通過這個lua上下文對象執行的lua代碼中就可以這么寫:
-- lua代碼,測試導入lua世界的native functions -- call function without returnsnative.saySomething() -- call function with a single number-typed returnlocal a = 10local b = 100local result = native.add(a, b)print(" a + b = ", result) --輸出 ?a + b = 110-- call function with two string-typed returns ? ? local str = "hello world!"local transformed, original = native.transformToUpper(str)print("original:", original, " transformed:", transformed) --輸出original: ? ?hello world! ? ? transformed: ? ?HELLO WORLD!看吧,現在已經成功地在C這棟“樓”和lua這棟“樓”之間搭建了一座(簡陋的)通道了!當然,Objective-C由于本身是C語言的一個超集,所以可以利用C的中間層,實現Objective-C實現的類成員方法和Lua的相互調用,具體步驟對諸君而言都沒什么難度,故在此不再贅言。
靜態binding,還是動態binding?
上述這種通過明確的接口列表注冊要導入Lua世界的C函數的方式,我們稱其為靜態binding。既然有靜態的,那當然也會有動態的binding——而這仰賴于Objective-C強大的runtime特性,最典型的一個框架就是Wax。引用Wax之后,就避免了Objective-C的方法都必須預先導入才能從Lua中調用的繁瑣,而是直接可以在lua腳本中編寫如下代碼就可以為Wax正確解析并執行:
-- lua code view = UIView:initWithFrame(CGRect(0, 0, 320, 100))-- all methods available to a UIView object can be accessed this way view:setBackgroundColor(UIColor:redColor())亦或
-- Created in "MyController.lua"-- -- Creates an Objective-C class called MyController with UIViewController-- as the parent. This is a real Objective-C object, you could even -- reference it from Objective-C code if you wanted to. waxClass{"MyController", UIViewController}function init()-- to call a method on super, simply use self.superself.super:initWithNibName_bundle("MyControllerView.xib", nil) ?return selfendfunction viewDidLoad()-- Do all your other stuff hereend甚至支持block:
-- lua code UIView:animateWithDuration_animations_completion(1, toblock( ? ? ? ?function()label:setCenter(CGPoint(300, 300))end), ? ?toblock(function(finished)print('lua animations completion ' .. tostring(finished))end,{"void", "BOOL"}))是不是和Objective-C代碼在函數傳遞、普通和匿名方法寫法上存在一些區別之外,幾乎一模一樣?這就是將Lua的元方法(Meta Method)特性和Objective-C強大的運行時能力組合起來得到的。
元方法+運行時調用這對組合好像很厲害
從名字來看,“元”這個字似乎是很強力的一種修飾,譬如元帥、元首、元氣幾個詞莫不如是,那么元方法呢,其實也是同樣。元方法其實是為為Lua中table對象設置的元表中關聯方法的總稱(基于lua5.1版本),其中很重要的兩個就是?index 以及?newindex兩個方法。通過key從table對象中獲取關聯的value失敗時,index方法會被觸發;而newindex則是在向table對象首次設置某key-value的鍵值對的時候會被觸發。自然,可以設想在lua代碼執行的時候,碰到不存在的變量譬如UIView時可以觸發全局的元方法,而如果其元方法乃是native的C 函數,則可以將"UIView"這個key傳入到native中區。 然后利用runtime方法譬如 NSClassFromString就可以獲取的真正的UIView類;而調用的方法,可以通過同樣的方式傳入native,再利用 instanceMethodForSelector/methodForSelector/methodSignatureForSelector等獲得相應的實現,并發起調用(其實為了兼容64位系統,最終是把函數調用封裝為NSInvocation調用并去派發的,此處暫不多做介紹)。
在native中完成了類實例方法/類方法的調用獲得了返回值之后,要怎樣遞送回lua的世界中呢?通常不同語言對于基礎數值類型(primitive value)都有相應支持,以之前自定義的3個C函數為例,會發現其中有這樣的邏輯:
lua_pushstring(L, "Now Native Talking");?lua_pushnumber(L, first + second);
很明顯,是通過為不同基礎類型變量提供顯式傳遞方法以便將值傳入到lua的世界中去。如果是Objective-C的類實例對象呢,為此,lua單獨提供了userdata類型,可以將native對象封裝為可以在lua世界中存在變量。
現在,我們可以實現在lua中觸發native對象的生成,并將其封裝成userdata傳遞會lua的世界,是不是就天下太平了?很明顯,這里還存在一個問題,那就是同一個對象,可能同時在native和lua都被引用著,那么它的生命周期該如何控制呢?畢竟兩個世界是遵循著不同的內存管理策略——Objective采用的是引用計數(ARC/MRC),而lua則是GC。解決這個問題同樣是利用了lua元方法gc,若為userdata類型變量關聯該方法,那么在其所有引用都已斷開的時候,即lua將要對其進行對象銷毀和內存回收時,會觸發gc的調用;如此一來,可以在__gc的C函數實現中將native對象的引用計數做相應遞減,代表lua世界也已不再引用該對象;可以按照Objective-C的正常規范,在其引用計數歸零時觸發native的回收機制。
總的來說,其簡略架構圖如下:?
靜態、動態binding對比
同樣是是實現不同語言之間的相互調用,但靜態binding和動態binding采用完全不同的設計思想,設計上的差異同樣會帶來使用上的區別。
首先很明顯的是,如果是靜態binding,那么每一個導入另一種語言世界的方法都需要手動實現(但有些工具可以極大簡化這種繁瑣,例如SWIG);而動態binding則利用了Objective-C強大的runtime特性和lua的元方法特性,可以省卻方法手動導入這一步驟。但動態binding這種便利總是有代價的,最大的問題就是性能消耗比靜態binding要高出不少,這也是ColorTouch移動端UI開發框架采用靜態binding為主,動態binding為輔的設計方案的主要原因。
混合編程的核心問題
上面啰啰嗦嗦說了好多,其實歸結起來,若要實現混合編程,最主要的幾個核心問題就是:
不同語言世界之間數值、對象、變量的轉化;
為另一個語言函數傳入的參數,以及從另一個語言世界獲得方法返回值的處理;
跨語言世界存在的對象,其生命周期管理。
總的來說,這三點就是我個人認為,如果要處理跨語言的編程問題,要最為關注的要點。把握、解決了這些要點,其他的問題相信就會迎刃而解!
·?EDN?·
作者:網易杭州研究院 ·?魏煒
網易云信|IM快速開發黑科技
ID:neteaseim ?長按識別,關注精彩
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的混合编程黑科技:跨语言编程问题迎刃而解的3个要点的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 将这五个原则变成习惯,你的开发经验更值钱
- 下一篇: BoBo接入云信,直播互动“连麦抢麦”分