首页 > 技术文章 > 一种C#泛型方法在lua中表示的设计

tiancaiwrk 2019-03-07 17:11 原文

  在进行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那边该怎样注册才能让这个调用能够实现呢? 一般来说我们调用泛型方法必须在写代码的时候就确定, 像这样:

Foo.GetTypeName<int>();  // 输出 Int32

而lua并不能这样约束, 它的调用必须还是非泛型的才可以, 这是第一个问题, 而第二个问题是lua那边怎样写? 我们希望它的写法能跟C#保持

一致, 或者相似吧, 让人看起来容易明白, 可是lua中中括号是大于小于号, 不能这样写, 想想有没有什么办法

因为在lua中是没有类型的, 类型必须来自C#, 所以只能将泛型作为非泛型方法才能使用, 如果让函数进行一次退化和封装, 像下面这样

-- 先将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<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应该是一个已经注册的类型.

[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  我们可以把 lua全局变量int 赋值为 typeof(int), 这样可以跟C#更加相似:
int =
typeof(int);

Foo.GetTypeName(int)();
Foo.GetTypeName<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)); 就不行, 需要后续测试

 


推荐阅读