在進行lua方法註冊的時候, 大多數解決方案直接否認了泛型方法, 由於在lua側難以表達出泛型, 以及lua的函數重載問題,閉包
函數重載問題能夠經過一些特殊方法解決, 而泛型問題是主要問題, 以Unity + Slua的狀況來講編輯器
好比下面的類: 函數
public class Foo { public static void GetTypeName(System.Type type) { Debug.Log(type.Name); } public static void GetTypeName<T>() { Debug.Log(typeof(T).Name); } }
通常只會生成 GetTypeName(System.Type type) 的註冊方法.測試
那麼泛型的方法在Lua那邊該怎樣註冊才能讓這個調用可以實現呢? 通常來講咱們調用泛型方法必須在寫代碼的時候就肯定, 像這樣:ui
Foo.GetTypeName<int>(); // 輸出 Int32
而lua並不能這樣約束, 它的調用必須仍是非泛型的才能夠, 這是第一個問題, 而第二個問題是lua那邊怎樣寫? 咱們但願它的寫法能跟C#保持lua
一致, 或者類似吧, 讓人看起來容易明白, 但是lua中中括號是大於小於號, 不能這樣寫, 想一想有沒有什麼辦法spa
由於在lua中是沒有類型的, 類型必須來自C#, 因此只能將泛型做爲非泛型方法才能使用, 若是讓函數進行一次退化和封裝, 像下面這樣code
-- 先將C# 的typeof註冊成全局函數, 註冊System.Int32命名爲int local Foo = {} Foo.GetTypeName = function(type) return function() print(type.Name) end end
Foo.GetTypeName(typeof(int))(); -- lua
Foo.GetTypeName<typeof(int)>(); // C#
這樣寫的話, 除了尖括號, 基本就能兩邊一致了對吧, 運行結果也是同樣的
/*至於怎樣註冊typeof(int)*/
// 在LuaState的Init中註冊個全局函數
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] internal static int getType(IntPtr L) { System.Type type = null; LuaObject.checkType(L, 1, out type); LuaObject.pushObject(L, type); return 1; }
// 在LuaState的Init中本身註冊咯
LuaDLL.lua_pushcfunction(L, getType);
LuaDLL.lua_setglobal(L, "typeof");
// CustomExport.OnAddCustomClass 中添加類型別名 add(typeof(System.Int32), "int"); // int
只是這裏lua的函數沒有進行C#那邊的調用啊, 下一步就來看看有沒有什麼辦法來實現調用.對象
若是經過自動註冊的話, Foo應該是一個已經註冊的類型.blog
[SLua.CustomLuaClass] public class Foo
而且有元表, 元表裏面有非泛型的GetTypeName方法了. 如今先不要去動元表,
直接註冊這個到Table裏面, 由於若是Table裏面有值的話, 就不會去查詢元表了
import "Foo"; Foo.GetTypeName(typeof(int)); // 輸出 Int32 rawset(Foo, "GetTypeName", function(type) return function() local mt = getmetatable(Foo) local func = rawget(mt,"GetTypeName"); func(type) end end) Foo.GetTypeName(typeof(int))(); // 輸出 Int32 -- 注意返回了function而後再次調用
這個方法比較流氓, 由於直接默認了有非泛型函數, 而且覆蓋了元表的非泛型方法, 不可取的.
要繼續的話, 首先來看看一個泛型方法怎樣經過非泛型(Type)方法進行調用的:
var methods = typeof(Foo).GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod); foreach(var method in methods) { if(method.IsGenericMethod) { var paramters = method.GetParameters(); if(paramters == null || paramters.Length == 0) { var genericMethod = method.MakeGenericMethod(new Type[] { typeof(int) }); if(genericMethod != null) { genericMethod.Invoke(null, null); // 輸出 Int32
break;
} } } }
固然是反射啦, 這樣就能讓泛型方法退化爲非泛型了, 雖然是一個緩慢的反射, 不過期間基本只花費在Invoke上, 問題還不大.
剩下的問題是重載了, 有非泛型和泛型的兩個同名函數, 爲了測試我先刪除掉非泛型,
[SLua.CustomLuaClass] public class Foo { //public static void GetTypeName(System.Type type) //{ // Debug.Log(type.Name); //} public static void GetTypeName<T>() { Debug.Log(typeof(T).Name); } }
生成的lua註冊代碼也要修改一下 (自動生成的註冊類文件名應該是Lua_Foo.cs 吧)
System.Type a1; checkType(l,1,out a1); Foo.GetTypeName(a1); // 泛型函數被註釋了 pushValue(l,true);
改爲
System.Type a1; checkType(l,1,out a1); var methods = typeof(Foo).GetMethods(System.Reflection. BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.InvokeMethod); foreach(var method in methods) { if(method.IsGenericMethod) { var paramters = method.GetParameters(); if(paramters == null || paramters.Length == 0) { var genericMethod = method.MakeGenericMethod(new Type[] { typeof(int) }); if(genericMethod != null) { genericMethod.Invoke(null, null); break; } } } } pushValue(l,true);
試試運行一下看看, 輸出 Int32 看來沒有問題, 問題是在Lua那邊仍是須要手動封裝了一遍, 看前文:
-- 問題是, 不進行一次rawset沒法獲得泛型寫法 Foo.GetTypeName(typeof(int))(); // 輸出 Int32 -- Table方法
到這裏, 基本就能夠得出結論了,
一. 在lua中能夠經過封裝(閉包)的方式接近C#的泛型的寫法, 差異只是一箇中括號和小括號
Foo.GetTypeName(typeof(int))(); -- lua Foo.GetTypeName<typeof(int)>(); // C#
然而過程異常複雜, 好比上述代碼中的rawset過程須要在C#的註冊代碼中進行實現, 而在調用的地方須要經過反射, 而且在lua側須要解決函數重載的問題,
上面的例子直接作了覆蓋. 就沒法正常訪問非泛型方法函數了, 是個錯誤方向.
PS: 今天又看了一遍這篇文章, 這裏有點歧義, 實際上是在lua這邊不須要實現重載, 重載方法的調用是由C#那邊的註冊代碼封裝的.
而我這裏手動覆蓋了lua這邊的Foo的GetTypeName方法(該方法原來由metatable提供, 如今我在Foo裏面直接添加了GetTypeName).
二. 既然泛型方法能夠退化爲非泛型, 那麼能夠直接檢測有沒有同名的且同參數的非泛型函數, 若是沒有就把泛型方法的非泛型版添加到註冊函數中便可.
Slua是經過反射程序集來查找相應類型而後經過反射裏面的對象來實現註冊代碼生成的, 這裏我想到一個喪心病狂的方法, 就是經過ILGenerator和Emit的方式,
把泛型方法轉換成非泛型方法, 添加到IL代碼裏面, 這樣在編輯器下的反射就能夠自動生成一個非泛型的對應函數了哈哈哈哈哈哈......想一想工做量真夠大的.
先封裝一下非泛型調用泛型方法的邏輯:
// 反射調用泛型函數方法 public static void CallGenericFunction(System.Type type, string genericFuncName, object instance, Type[] genericTypes, object[] paramaters, bool isStatic) { var flags = BindingFlags.Public | BindingFlags.NonPublic | (isStatic ? BindingFlags.Static : BindingFlags.Instance) | BindingFlags.InvokeMethod; var methods = typeof(Foo).GetMethods(flags); foreach(var method in methods) { if(method.IsGenericMethod && string.Equals(method.Name, genericFuncName, StringComparison.Ordinal)) { var arguments = method.GetGenericArguments(); // 檢查泛型類的數量是否對的上 if(arguments != null && arguments.Length == genericTypes.Length) { // 檢查傳入參數類型是否對的上, 若是考慮到可變參數, default value參數, 可空結構體參數等, 會很複雜 if(MethodParametersTypeEquals(method, paramaters)) { var genericMethod = method.MakeGenericMethod(genericTypes); if(genericMethod != null) { genericMethod.Invoke(instance, paramaters); break; } } } } } } // 簡單的對比一下, 實際使用要考慮到可變參數( params object[] ), default value參數( bool isStatic = false ), 可空結構體參數( int? a = null )等 public static bool MethodParametersTypeEquals(MethodInfo method, object[] parameters) { var mehotdParamters = method.GetParameters(); int len_l = mehotdParamters != null ? mehotdParamters.Length : 0; int len_r = parameters != null ? parameters.Length : 0; return len_l == len_r; }
測試一下, 爲了測試非靜態函數, 添加了非靜態一個方法
[SLua.CustomLuaClass] public class Foo { public static void GetTypeName(System.Type type) { Debug.Log(type.Name); } public static void GetTypeName<T>() { Debug.Log(typeof(T).Name); } public void TypeName<T>() { Debug.Log(typeof(T).Name); } }
CallGenericFunction(typeof(Foo), "GetTypeName", null, new Type[] { typeof(float) }, null, true); // 輸出 Single var foo = new Foo(); CallGenericFunction(typeof(Foo), "TypeName", foo, new Type[] { typeof(int) }, null, false); // 輸出 Int32
調用是正確的.
寫了這個調用封裝以後, 我發現有兩種路線能夠進行下去, 第一個就是前面提到的Emit(只在編輯器下使用, 能夠放心), 另外一個方法是修改Slua的生成代碼的邏輯,
先說修改Slua的生成代碼那方面, 上面的Foo生成的GetTypeName的註冊函數以下 :
[SLua.MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] [UnityEngine.Scripting.Preserve] static public int GetTypeName_s(IntPtr l) { try { System.Type a1; checkType(l, 1, out a1); Foo.GetTypeName(a1); pushValue(l, true); return 1; } catch(Exception e) { return error(l, e); } }// 刪了一些頭頭尾尾
若是沒有非泛型方法GetTypeName(System.Type type), 它是不會有這個註冊函數的, 咱們先直接改這裏:
//Foo.GetTypeName(a1); Test.CallGenericFunction(typeof(Foo), "GetTypeName", null, new Type[] { a1 }, null, true); // 直接調用換成了封裝的調用, 函數放在一個Test類裏面
lua調用進入這裏, 注意這裏是非泛型方法的入口, 我改了C#那邊把調用轉到了泛型方法
import "Foo"; Foo.GetTypeName(typeof(int)); // 輸出 Int32
正確的, 那麼應該修改的生成代碼邏輯在 LuaCodeGen.cs 裏面, 具體就不測試了, 邏輯就改成在反射獲取泛型方法以後, 將泛型方法跟非泛型作對比, 而後採起合併之類的邏輯進行代碼生成. 這個就看本身的控制邏輯了,
由於退化後的泛型跟非泛型同名同然而邏輯不一樣的狀況是存在的, 若是邏輯不一樣的話, 就麻煩了...
public static void GetTypeName(System.Type type); // 邏輯1 public static void GetTypeName<T>();// 退化->GetTypeName(System.Type type) 邏輯2
在只有GetTypeName<T>()方法時, 那就像上圖的生成代碼替換掉Foo.GetTypeName(a1) 爲 Test.CallGenericFunction(typeof(Foo), ...) 便可.
兩個方法都有時, 任選其一便可. 固然存在函數重載的實現也在這裏進行便可, 原來的函數重載就是在這實現的.
第二種: 把泛型方法轉換成非泛型而後添加到原有類型中, ILGenerator 太複雜, 寫個例子就好了, 再見!!!
PS: 這個使用方法不能對已經存在的類型進行修改, 是建立了一個類型, 固然咱們能夠把有泛型的類型生成一個新的類型來用非泛型代替泛型, 而後利用lua實現多重繼承或者強行註冊的方式實現最終的整合.
然而, 建立新類型去調用原有類型的泛型方法, 不是靜態的須要傳遞實例做爲參數, 而後在自動生成代碼處自動封裝, 這些邏輯會複雜上天了.
public class Test{ public static System.Type TestILGenerator() { const string funcName = "SayHello"; //構建程序集 var asmName = new AssemblyName("Test"); var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave); //構建模塊 var mdlBldr = asmBuilder.DefineDynamicModule("Main", "Main.dll"); //構建類 var typeBldr = mdlBldr.DefineType("Hello", TypeAttributes.Public); //構建方法 var methodBldr = typeBldr.DefineMethod( funcName, MethodAttributes.Public | MethodAttributes.Static, null,//return type null//parameter type ); //IL構建底層細節 var il = methodBldr.GetILGenerator();//獲取il生成器 il.Emit(OpCodes.Ldstr, "Hello, World"); il.Emit(OpCodes.Call, typeof(UnityEngine.Debug).GetMethod("Log", new Type[] { typeof(string) })); il.Emit(OpCodes.Ret); //完成構建 var t = typeBldr.CreateType(); t.GetMethod(funcName).Invoke(null, null); // 輸出 Hello, World return t; } }
我在這裏測試了一下, 建立類型Hello以及設置靜態函數SayHello都是成功了的, 這樣就欺騙了編譯器認爲這個類型存在了, 用LuaCodeGen也能強行生成註冊類函數:
[MenuItem("SLua/Custom/Make2")] // 強行搞一個代碼建立 static public void Custom2() { List<Type> exports = new List<Type>(); string path = GenPath + "Custom/"; ExportGenericDelegate fun = (Type t, string ns) => { if(Generate(t, ns, path)) exports.Add(t); }; var myType = Test.TestILGenerator(); fun(myType, null); }
生成出的代碼就懵逼了哈哈, 在生成的時候騙了編譯器, 生成以後是騙不了人的, 下面的生成的Lua_Hello.cs 文件.
[UnityEngine.Scripting.Preserve] public class Lua_Hello : LuaObject { [SLua.MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] [UnityEngine.Scripting.Preserve] static public int constructor(IntPtr l) { try { Hello o; o=new Hello(); // 注意這裏編譯錯誤, 由於沒有實體的類型, 編譯器沒法找到Hello類 pushValue(l,true); pushValue(l,o); return 2; } catch(Exception e) { return error(l,e); } } [SLua.MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))] [UnityEngine.Scripting.Preserve] static public int SayHello_s(IntPtr l) { try { Hello.SayHello(); pushValue(l,true); return 1; } catch(Exception e) { return error(l,e); } } [UnityEngine.Scripting.Preserve] static public void reg(IntPtr l) { getTypeTable(l,"Hello"); addMember(l,SayHello_s); createTypeMetatable(l,constructor, typeof(Hello)); } }
這種方法到這裏基本上就是死刑了, 但是還能夠搶救一下, 就是在運行時進行類型的生成, 而後Lua註冊代碼, 也就是Lua_Hello.cs裏面這些, 也在運行時生成, 而後完成運行時的註冊行爲...
恩...因此說來講去, 最方便的仍是在C#中寫上非泛型方法最方便了, 本文也是突發奇想研究弄一下泛型方法在lua中的自動註冊問題.
這裏的簡單例子中泛型和非泛型沒有嚴格的使用限制, 因此能夠在C#中兩種都寫, 實際狀況可能有些狀況寫不了非泛型的狀況也有, 這種狀況下經過lua調用CallGenericFunction方法來調用泛型方法反而比自動註冊簡單多了...
脫褲子放屁的事情就別作了, 直接調反射吧.
補充(2019.04.16):
MakeGenericMethod 這個方法在AOT編譯下能不能使用還不清楚, 由於類型不肯定的時候是在運行時生成類型, 好比泛型類在運行時生成就不行,
好比 AA<T> 這個泛型類, 使用 typeof(AA<>).MakeGenericType(typeof(int)); 就不行, 須要後續測試