天天看點

基于LuaInterface了解跨語言互動的原理

文章目錄

    • 一、跨語言調用的定義及四個疑問
    • 二、了解C#與Lua間的跨語言調用
      • 1. lua的簡單介紹
      • 2. LuaInterface是什麼
      • 3. LuaInterface的結構
      • 4.KopiLua的實作方式
      • 5. 解開文章開頭的四個疑問
        • 1. 兩個語言之間的聯系是如何建立起來的?
        • 2. 一個資料是怎樣從一個語言轉換到另一個語言的?
        • 3. 一個語言是如何實作對另一個語言的函數的調用的?
        • 4. 兩個語言之間如何查找到對應的值或函數?
      • 6. 總結:LuaInterface所做的工作
    • 三、虛拟機
      • 1. 虛拟機的定義
      • 2. 常見的虛拟機及對應的程式設計語言
    • 四、跨語言調用的核心要素

一、跨語言調用的定義及四個疑問

  顧名思義,跨語言調用,就是在兩個不同語言之間傳遞資料或者互相調用函數。

  很多寫過代碼的人都有過跨語言調用的經曆,比如寫安卓的要處理C++代碼和java代碼的互動,寫ios的要處理C++、swift和OC代碼的互動;寫遊戲用戶端的就更别說了,如果是在國内做unity遊戲的,C++、java、lua、c#、oc混在一起是常事;寫伺服器的也常有golang和c++互動、java和js互動的。

  總而言之,不同語言之間的互相調用還是相當常見的,我在面試中也不止一次被問到過“C#是如何跟lua互動”這樣的問題,雖說以前也對這些有過了解,但通常僅僅隻知道一些使用方式,如果細究到具體的實作原理,卻又遇到了四個不清不楚的疑問——

  1. 兩個語言之間的聯系是如何建立起來的?
  2. 一個資料是怎樣從一個語言轉換到另一個語言的?
  3. 一個語言是如何實作對另一個語言的函數的調用的?
  4. 兩個語言之間是如何互相查找到需要的值或函數的?

二、了解C#與Lua間的跨語言調用

  這個問題範圍其實很大,于是我找了一個切入點,選擇的是LuaInterface。

1. lua的簡單介紹

  雖然這裡是以lua為切入點來了解跨語言調用,是以有必要簡單地介紹一下lua和lua的機制,不過這篇文章不會涉及到Lua代碼,是以僅做簡單介紹,有所了解即可。

  1. 首先是官網的介紹:

    What is Lua? Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description.

    Lua是一種強大、高效、輕量級、可嵌入的腳本語言。它支援程式性程式設計、面向對象程式設計、函數式程式設計、資料驅動程式設計和資料描述。

    Lua combines simple procedural syntax with powerful data description constructs based on associative arrays and extensible semantics. Lua is dynamically typed, runs by interpreting bytecode with a register-based virtual machine, and has automatic memory management with incremental garbage collection, making it ideal for configuration, scripting, and rapid prototyping.

    Lua将簡單的程式性文法與基于關聯數組和可擴充語義的強大資料描述結構相結合。Lua是動态類型的,通過解釋基于寄存器的虛拟機的位元組碼來運作,并具有自動記憶體管理和增量垃圾收集功能,使其成為配置、腳本和快速原型設計的理想選擇。

      劃重點:通過解釋基于寄存器的虛拟機的位元組碼來運作,基于虛拟機的運作機制,結合lua小巧的特點,使它變得極其容易作為一個嵌入式語言運作在宿主之中。
  2. lua在設計上的核心要點,是它巧妙的堆棧設計,這也成為了它易用性的重要來源之一。
  3. lua原生是使用c語言編寫的,可以簡單地編譯為各個平台的native語言,通常情況下,多數語言都會提供與native語言的互動接口,适應性更強。

2. LuaInterface是什麼

  LuaInterface這個單詞很直白,從構詞上就能看出來,是一個Lua的互動界面,準确地說,是Lua與CLR的互動接口,在LuaInterface指引中有這樣一段文字:

LuaInterface is library for integration between the Lua language and Microsoft .NET platform’s CommonLanguage Runtime (CLR) .

LuaInterface是Lua語言和微軟.NET平台的通用語言運作時(CLR)之間的內建庫。

  那麼它能做什麼呢?一句話,實作C#和lua之間的互相調用,目前國内見到的很多在C#、尤其是unity中使用的Lua插件都是直接或間接、基于或派生自LuaInterface的,包括ToLua#、NLua、ULua等等。

3. LuaInterface的結構

  LuaInterface在2013年左右就停止了更新,但我們仍然可能通過它的源碼來知道它到底是如何實作的。源碼可以在這裡下載下傳到:LuaInterface的Git。

将源碼下載下傳下來以後打開就可以看到,原始LuaInterface包括兩個核心部分,一部分是使用C#實作的Lua虛拟機KopiLua,另一部分是使用C#實作的資料轉換和綁定接口。

基于LuaInterface了解跨語言互動的原理

4.KopiLua的實作方式

  這個問題如果細說起來非常複雜,但是如果僅從抽象上了解,可以簡單地用一句話概括:使用C#的類型建立了lua的基礎資料模型,然後在這個基礎上建立了運算規則,最後使用c#實作了lua的文法分析和虛拟機。

  如果有興趣的話,可以通過《自己動手實作Lua》 機械工業出版社,張秀宏著這本書來深入學習,自己造一個lua。

  使用過lua的同學都知道,lua有八種基礎資料類型,分别為:nil、boolean、number、string、userdata、function、thread 和 table,在KopiLua的源碼中我們可以一一找到,比如lua中的number是一個簡單的别名:

比如在lua中的string,在KopiLua中是這樣定義的:

public class TString : TString_tsv{ ... }//TString就是KopiLua模型中lua語言裡的string
public class TString_tsv : GCObject {...}
public class GCObject : GCheader, ArrayElement{...}
           

  其他的資料類型也類似,因為KopiLua本身就是使用C#建立的lua資料,是以實際上我們使用的lua資料本質上都是C#的資料。

  此外,KopiLua中還有一個叫

TValue

的容器類,這個容器類可以封裝任何類型,用于作為一個中介轉換,效果類似于C#中的object。更詳細的介紹可以檢視這篇文章,雖然該文章講解的是C語言的lua實作,但是實作思想是完全一樣的:lua堆棧。

5. 解開文章開頭的四個疑問

回到我在文章開頭的那四個問題:

  1. 兩個語言之間的聯系是如何建立起來的?
  2. 一個資料是怎樣從一個語言轉換到另一個語言的?
  3. 一個語言是如何實作對另一個語言的函數的調用的?
  4. 兩個語言之間是如何互相查找到需要的值或函數的?

接下來我把它們當洋蔥一個問題一個問題地剝開。

1. 兩個語言之間的聯系是如何建立起來的?

  在知道了LuaInterface的結構後,這個問題的答案其實已經擺在桌面上了——KopiLua本來就是用C#實作的,在C#啟動KopiLua的虛拟機的時候,lua就作為一個運作環境在C#中跑起來了,這個lua本就是在C#環境中運作的,自然可以通路C#中的記憶體,它們之間的互動隻是普通的資料交換,同樣,C#也可以通路lua中的記憶體。換句話說,它們本來就在同一個環境裡。

  這是非常重要的一點,一定要時刻記住,這兩種語言的環境一開始就是一緻的,隻是對程式員而言所面對的環境是不同的,我們所操作的隻不過是運作環境提供給我們的一些可用于編寫邏輯的接口,在實際運作過程中,它們始處于相同的環境,隻是資料模型存在差别。

2. 一個資料是怎樣從一個語言轉換到另一個語言的?

  這個問題包括兩方面,一是如何從KopiLua中的lua資料轉換到C#中的資料,二是如何從C#中的資料轉換到KopiLua中的lua資料。

  • LuaInterface中的資料互通方式

      在LuaInterface指引中,我們知道,将資料轉入轉出是通過這樣的方式:

    // Start a Lua interpreter
    Lua lua = new Lua();
    // Create global variables "num" and "str"
    lua["num"] = 2;
    lua["str"] = "a string";
    // Create an empty table
    lua.NewTable("tab");
    // Read global variables "num" and "str"
    double num = (double)lua["num"];
    string str = (string)lua["str"];
               

  可以看到這裡使用了C#的索引器來實作,打開

Lua

這個類,找到索引器實作。

public object this[string fullPath]
{
	get 
	{
		object returnValue = null;
		int oldTop = LuaLib.lua_gettop(luaState);
		string[] path = fullPath.Split(new char[] { '.' });
		LuaLib.lua_getglobal(luaState, path[0]);
		returnValue = translator.getObject(luaState, -1);//核心擷取的調用

		if(path.Length>1) 
		{
			string[] remainingPath = new string[path.Length-1];
			Array.Copy(path, 1, remainingPath, 0, path.Length-1);
			returnValue = getObject(remainingPath);
		}

		LuaLib.lua_settop(luaState, oldTop);
		return returnValue;
	}
	set 
	{
		int oldTop = LuaLib.lua_gettop(luaState);
		string[] path = fullPath.Split(new char[] { '.' });

		if(path.Length == 1) 
		{
			translator.push(luaState, value);//核心set的調用
			LuaLib.lua_setglobal(luaState, fullPath);
		} 
		else 
		{
			LuaLib.lua_getglobal(luaState, path[0]);
			string[] remainingPath = new string[path.Length-1];
			Array.Copy(path, 1, remainingPath, 0, path.Length-1);
			setObject(remainingPath, value);
		}

		LuaLib.lua_settop(luaState, oldTop);

		// Globals auto-complete
		if(value.IsNull())
		{
			// Remove now obsolete entries
			globals.Remove(fullPath);
		}
		else
		{
			// Add new entries
			if(!globals.Contains(fullPath))
				registerGlobal(fullPath, value.GetType(), 0);
		}
	}
}
           
  • 首先來看如何從C#中的資料到Lua資料

      閱讀一下代碼,會發現從C#到lua的轉換出現在set索引器, 核心的函數是

    translator.push

    ,其中

    setObject

    中也是通過

    translator.push

    來實作的。再深入一下,找到調用的最核心部分,會發現這樣的代碼:
    internal static void setnilvalue(TValue obj) {
    	obj.tt=LUA_TNIL;
    }
    
    internal static void setnvalue(TValue obj, lua_Number x) {
    	obj.value.n = x;
    	obj.tt = LUA_TNUMBER;
    }
    
    internal static void setsvalue(lua_State L, TValue obj, GCObject x) {
    	obj.value.gc = x;
    	obj.tt = LUA_TSTRING;
    	checkliveness(G(L), obj);
    }
    
    internal static void setuvalue(lua_State L, TValue obj, GCObject x) {
    	obj.value.gc = x;
    	obj.tt = LUA_TUSERDATA;
    	checkliveness(G(L), obj);
    }
               

  這裡出現了

TValue

這個類,前面說到過,它隻作為一個中介的容器存在,它執行個體有可能代表lua中的任何值;每一次在進行set時,資料最後都會被轉成

TValue

,并且儲存在這個類執行個體中的

value

中。

  • 再來看看如何從lua到c#

      從lua到c#中的轉換在get索引器中, 核心擷取資料的方式是

    translator.getObject

    ,我們可以一步深入找到調用的根節點,最後我們會發現這個函數指向了這一段代碼:
internal static lua_Number nvalue(TValue o) { return (lua_Number)check_exp(ttisnumber(o), o.value.n); }
internal static TString rawtsvalue(TValue o) { return (TString)check_exp(ttisstring(o), o.value.gc.ts); }
internal static TString_tsv tsvalue(TValue o) { return rawtsvalue(o).tsv; }
internal static Udata rawuvalue(TValue o) { return (Udata)check_exp(ttisuserdata(o), o.value.gc.u); }
internal static Udata_uv uvalue(TValue o) { return rawuvalue(o).uv; }
internal static Closure clvalue(TValue o) { return (Closure)check_exp(ttisfunction(o), o.value.gc.cl); }
internal static Table hvalue(TValue o) { return (Table)check_exp(ttistable(o), o.value.gc.h); }
           

  以number為例,

check_exp

函數僅用于檢查資料,

ttisnumber

函數用于檢查一個

TValue

類型的值是否是

number

,最後的傳回值是TValue中的value變量裡的n,這個n就代表number;類似的,如果是string那麼就調用

rawtsvalue

函數,如果是function就調用

clvalue

函數。

  得到了最後的值以後,再通過資料強類型轉換,就能得到一個所需類型的C#中的資料了。

  • LuaInterface資料調用的總結

      通過TValue這樣一個值作為抽象,實作了C#中的值與lua值之間的映射,因為KopiLua本來就運作在C#上,是以資料隻是換一個模型進行存儲;

      使用lua腳本建立的值在轉換為C#值時會通過TValue封裝,然後轉換成C#的object,最後強制轉換成對應的類型;

      使用C#存入lua環境中的值一開始就是通過TValue進行封裝的,取出後再強制轉換。

3. 一個語言是如何實作對另一個語言的函數的調用的?

  我們知道在lua裡,function也是基本類型之一,是以在KopiLua中就對function進行了模組化,以

的形式将一個C#方法通過委托封裝在其内部,并且增加了從C#層直接調用的

Call

方法,這樣一來兩個語言之間的調用就解決了:

  • 如果是一個C#注冊進入lua的函數,那麼這是一個委托,運作時會回到C#層中對應的方法中運作;
  • 如果是一個lua内部的函數,将在Lua虛拟機中運作(當然因為是C#實作,運作時還是以C#的形式運作)。

LuaInterface automatically converts Lua’s nil to CLR’s null, strings to System.String, numbers to System.Double, booleans to System.Boolean, tables to LuaInterface.LuaTable, functions to LuaInterface.LuaTable, and vice-versa.

LuaInterface自動将Lua的nil轉換為CLR的null,字元串轉換為System.String,數字轉換為System.Double,布爾轉換為System.Boolean,表格轉換為LuaInterface.LuaTable,函數轉換為LuaInterface.LuaTable,反之亦然。

4. 兩個語言之間如何查找到對應的值或函數?

  • C#中如何查找lua中的對象

      在前面我們已經知道了從C#中擷取lua中的對象是通過

    Lua

    類的索引器進行的,這個索引器的核心邏輯基于lua的table機制,邏輯很簡單,如下面這個流程圖所示:

  因為Lua虛拟機在啟動時會建立一個全局的global table,是以一切的lua對象都可以從這個table中找到,如果找不到,那必然是不存在的。通過這樣簡單的邏輯就可能在lua中查找指定的對象。

  • lua中如何查找C#的對象

      LuaInterface中提供了兩種方式從lua中來查找C#中的對象。

    • 第一種,在C#中顯式注冊一個方法

        LuaInterface指引中有這樣一段話:

    Finally, class Lua also has the RegisterFunction method to register CLR methods as global Lua functions. Its parameters are the name of the variable where the function will be stored, the target of the method and the MethodInfo object representing the method, for example, lua.RegisterFunction(“foo”,obj,obj.GetType().GetMethod(“Foo”)) registers method Foo of object obj as function foo.

      最後,Lua類也有RegisterFunction方法來将CLR方法注冊為全局Lua函數。它的參數是存儲函數的變量名稱、方法的目标和代表該方法的MethodInfo對象,例如,lua.RegisterFunction(“foo”,obj,obj.GetType().GetMethod(“Foo”)) 将對象obj的方法Foo注冊為函數foo。

    • 第二種是通過反射來調用

        簡單來說,LuaInterface在啟動時,會通過第一種方式将一系列的基礎方法注冊到lua的global table中。

    /*
    	 * Registers the global functions used by LuaInterface
    	 */
    	private void setGlobalFunctions(LuaCore.lua_State luaState)
    	{
    		LuaLib.lua_pushstdcallcfunction(luaState, metaFunctions.indexFunction);
    		LuaLib.lua_setglobal(luaState, "get_object_member");
    		LuaLib.lua_pushstdcallcfunction(luaState, importTypeFunction);
    		LuaLib.lua_setglobal(luaState, "import_type");
    		LuaLib.lua_pushstdcallcfunction(luaState, loadAssemblyFunction);
    		LuaLib.lua_setglobal(luaState, "load_assembly");
    		LuaLib.lua_pushstdcallcfunction(luaState, registerTableFunction);
    		LuaLib.lua_setglobal(luaState, "make_object");
    		LuaLib.lua_pushstdcallcfunction(luaState, unregisterTableFunction);
    		LuaLib.lua_setglobal(luaState, "free_object");
    		LuaLib.lua_pushstdcallcfunction(luaState, getMethodSigFunction);
    		LuaLib.lua_setglobal(luaState, "get_method_bysig");
    		LuaLib.lua_pushstdcallcfunction(luaState, getConstructorSigFunction);
    		LuaLib.lua_setglobal(luaState, "get_constructor_bysig");
    	}
               

      在lua中調用C#的成員變量或方法時,會載入指定的assembly,并從中擷取正确的對象。

      更詳細的實作可以閱讀源碼或參考這篇文章:luanet的反射調用過程

6. 總結:LuaInterface所做的工作

  LuaInterface由KopiLua和LuaInterface橋兩部分組成,這兩者之中,KopiLua實作了lua的運作,而LuaInterface所做的事,就是充當橋梁,進行C#與lua之間資料的互通和函數的綁定。

  在使用LuaInterface時,在C#中實際的操作時序一般是這樣的:

總結性地說,LuaInterface做了下面兩件事:

  1. 建立了Lua與C#之間的映射,包括基礎類型的轉換、自定義類型到table的轉換、函數的轉換
  2. 建立了互相調用的機制,包括在C#和Lua兩端對另一端中對象的查找定位功能。

三、虛拟機

   讀者看到這裡可能會覺得有點突兀,怎麼突然就說到虛拟機了呢?

   我們回過頭來看看,Lua運作機制的核心是什麼?沒錯,就是虛拟機,籍由虛拟機,lua成功地在C#上跑了起來,進而為與C#之間進行互動提供了良好的基礎,否則如果是完全獨立的兩個程序,互動恐怕就要難得多了。

   是以為了更深入地了解兩個語言的互動,有必要對虛拟機有所了解。

1. 虛拟機的定義

   首先要解釋一下虛拟機的定義。在《計算機組成:結構化方法》 作者: 坦嫩鮑姆一書中,開篇就是把虛拟機作為核心概念進行介紹的,在他的定義裡,首先将計算機系統進行了分層,然後把虛拟機抽象為一個與語言存在重要對應關系的、可以直接執行于它上一層計算機(可以是硬體也可以是另一個虛拟機)的計算機;此外,還提出了兩種程式設計語言的執行方式:解釋型和編譯型。

   需要這本書電子版的可以通過這個連結下載下傳,有條件還是推薦購買紙質書籍閱讀:連結,提取碼:j4k5 ;也可以通過這個連結了解:什麼是虛拟機?。

 以前面提到過的KopiLua和C#為例,來對應一下上面說的虛機機概念。在這個環境中存在兩個虛拟機。

  • 第一個虛拟機是CLR,它運作在Native層上,支援C#在CLR上的運作,但在運作C#之前,需要将C#編譯成CIL碼,C#是一門編譯型語言;
  • 第二個虛拟機是KopiLua,它運作在CLR層上,它既可以通過解釋lua語言來運作,也可以提前編譯為位元組碼來運作,在lua虛拟機這一層上,lua位元組碼是Native的,可以說lua既是解釋型的,也是編譯型的。

   虛拟機這個名字其實不少見,也有人從不同的角度把虛拟機分成了不同的類型:虛拟機的分類,有興趣的可以深入了解一下。不過在這裡,對虛拟機的基本概念有所了解即可。

2. 常見的虛拟機及對應的程式設計語言

采用這種虛拟機概念的其實非常多,簡單列幾個如下:

虛拟機 語言 說明
.Net Framework C# 基于CLR
Mono C# 基于CLR
.Net Framework F# 基于CLR
Dalvik java 基于JVM
HotSpot VM java 基于JVM
J9 VM java 基于JVM,最流行的JAVA虛拟機
KopiLua lua C#的lua虛拟機實作
Lua lua C語言的官方标準lua虛拟機實作
luajit lua 相容lua語言的高效虛拟機,運作在luajit上的lua号稱最快的腳本語言
windows C++ 基于x86_64指令集,編譯後變成二進制指令
MacOS ObjectiveC 基于x86_64指令集,編譯後變成二進制指令
MacOS swift 基于x86_64指令集,編譯後變成二進制指令
MacOS(M1處理器版本) swift 基于ARM指令集,編譯後變成二進制指令

四、跨語言調用的核心要素

最後,讓我們來總結一下,跨語言調用的核心要素到底有哪些。

我認為有以下兩點:

  1. 處于同一個環境中,可以是被其中一個啟動,也可以是被一個第三方調用者連接配接,比如ToLua中使用的Lua虛拟機并不是使用C#實作的,而是使用C語言實作,編譯成了特定平台上的Native庫,但是在ToLua中利用C#的

    DllImport

    接口将lua的api引入了C#,一樣實作了lua虛拟機的運作。
  1. 中間層,用于進行資料轉換和子程式的映射以及提供查找指定對象的功能。

    注:這裡的“子程式”使用的是《代碼大全(第二版)》裡第七章“高品質的子程式”中的子程式概念,定義為為實作一個特定的目的而編寫的一個可被調用的方法或過程。

基于這兩個核心要素,我們也可以審視一下C#和java語言中用來實作跨語言互動的元件。

  • 實作java與C/C++互動的JNI
  1. java虛拟機運作于Native層
  2. C/C++編譯為Native代碼,JNI作為中間層提供了上面所說的資料轉換和子程式映射的功能。
  • 實作C#與C/C++互動的DllImport标記
  1. CLR運作于Native層
  2. C/C++編譯為Native代碼,DllImport标記函數後,C#編譯器在Native代碼中查找指定子程式

最後,我隻想念一句名言:

“Any problem in computer science can be solved by anther layer of indirection.”

“計算機科學的一切問題都可以通過增加一個中間層來解決”

繼續閱讀