0%

一种Lua反射调用C#机制的实现

一 概述

NativeChannel提供Lua层通过反射调用C#层代码的机制,支持对静态类和非静态类中属性、方法、委托的调用,并支持在Lua层使用宏定义对代码进行隔离。通过NativeChannel可以不生成额外的wrap文件来达到Lua层访问C#层的效果,调用更加灵活,适合需要集成到Lua层但又要灵活增删的模块(如SDK)。

二 实现

实现的大致思路为封装两个类AnyObjectAnyStaticObject暴露到Lua层,这两个类都实现IAny接口。这两个类包含对具体类中的生命周期、属性、方法、委托的所有操作。再定义通道类NativeChannel,实现INativeChannel和IServiceStart接口,包装成Service管理所有反射调用的类。

Object
  • Interface

AnyObjectAnyStaticObject都实现了IAny接口,它们之间的区别是前者储存了目标类实例的引用target,在使用时调用属性、方法需要传入该引用;而后者储存的是静态类的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   
public interface IAny
{
void OnStart();
void OnUpdate(float deltaTime);
void OnEnd();
object Get(string prop);
void Set(string prop, object value);
object Call(string method, params object[] args);
void AddDelegate(string del, string func, LuaTable self = null);
void AddDelegate(string del, LuaFunction func);
void RemoveDelegate(string del, string func, LuaTable self = null);
void RemoveDelegate(string del, LuaFunction func);
}
  • Life cycle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void OnStart()
{
var method = this.target.GetType().GetMethod("OnStart");
method?.Invoke(this.target, null);
}

public void OnUpdate(float deltaTime)
{
var method = this.target.GetType().GetMethod("OnRefresh");
Object[] p = new Object[] {deltaTime};
method?.Invoke(this.target, p);
}

public void OnEnd()
{
var method = this.target.GetType().GetMethod("OnEnd");
method?.Invoke(this.target, null);
}
  • Property
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public object Get(string prop)
{
return this.target.GetType().GetProperty(prop)?.GetValue(this.target);
}

public void Set(string prop, object value)
{
var propInfo = this.target.GetType().GetProperty(prop);
if (propInfo != null)
{
value = ConvertType(propInfo.PropertyType, value);
propInfo?.SetValue(this.target, value);
}
}
  • Method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public object Call(string method, params object[] args)
{
var methodInfo = this.target.GetType().GetMethod(method);
var parameters = methodInfo?.GetParameters();
if (args.Length != parameters?.Length)
{
Debug.LogError("Wrong parameters!");
return null;
}

for (int i = 0; i < parameters.Length; i++)
{
args[i] = ConvertType(parameters[i].ParameterType, args[i]);
}

return methodInfo?.Invoke(this.target, args);
}
  • Delegate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void AddDelegate(string del, string func, LuaTable self = null)
{
var eventInfo = this.target.GetType().GetEvent(del);
var eventHandlerType = eventInfo.EventHandlerType;
var eventArgs = eventHandlerType.GetMethod("Invoke")?.GetParameters();
Type type;
if (eventArgs == null)
{
type = null;
eventHandlerType = null;
return;
}

var args = new Type[eventArgs.Length];
for (int i = 0; i < eventArgs.Length; i++)
{
args[i] = eventArgs[i].ParameterType;
}

switch (eventArgs.Length)
{
case 0:
type = typeof(Handler);
break;
case 1:
type = typeof(Handler<>).MakeGenericType(args);
break;
//...
default:
type = null;
break;
}
if (type == null || eventHandlerType == null)
{
return;
}

var helper = Activator.CreateInstance(type, func, self);
var handler = Delegate.CreateDelegate(eventHandlerType, helper, "Handle");
eventInfo.AddEventHandler(this.target, handler);
}
NativeChannel
1
2
3
4
5
6
7
8
9
10
11
[LuaUsage]
public interface INativeChannel
{
[LuaIgnore]
void Register(string name, Func<object> factory);

IAny Get(string name);

[LuaIgnore]
void Define(string macro);
}

NativeChannel中维护一个字典factories存储所有反射调用的类,维护一个列表macros存储所有在Lua层需要添加的宏定义。

  • Register()

提供在C#层注册Object的方法

1
2
3
4
public void Register(string name, Func<object> factory)
{
this.factories.Add(name, () => new AnyObject(factory()));
}
  • Get()

暴露到Lua层,供Lua层通过类名获取对应的引用,若字典中存在该类型则直接返回,若不存在则创建对应的Object。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public IAny Get(string name)
{
if (this.factories.TryGetValue(name, out var factory))
{
return factory();
}

var type = Type.GetType(name);
if (type == null) return null;
if (type.IsAbstract && type.IsSealed)
{
return new AnyStaticObject(type);
}
else
{
var obj = Activator.CreateInstance(type);
if (obj == null) return null;
return new AnyObject(obj);
}
}
  • Define()

在C#层中添加宏定义。

三 问题与解决方法

类型转换

对于属性的赋值和方法的参数,Lua层传递过来的数据在C#中为Object类型,需要根据目标类型进行显式的转换。

1
2
3
4
5
6
7
8
private object ConvertType(Type type, object value)
{
if (type == typeof(short))
{
return Convert.ToInt16(value);
}
//...
}
委托的调用
  • 为了能正确调用委托,需要确定委托的参数和类型,因此需要特别封装各种参数数量的泛型类Handler,每种Handler类包含构造方法Handler()和触发回调方法的函数Handle()

  • 在委托添加方法时,先获取方法所有的参数,根据参数个数和每个参数的类型确定Handler的类型:type = typeof(Handler<>).MakeGenericType(args);

  • 接着使用var helper = Activator.CreateInstance(type, func, self);构造实例。

  • 最后绑定方法

    var handler = Delegate.CreateDelegate(eventHandlerType, helper, "Handle"); eventInfo.AddEventHandler(this.target, handler);

    至此,当委托触发时,就能调用到对应Handler类中的Handle()方法,通过该方法再触发Lua层对应的方法,实现动态的绑定调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Handler<T1, T2>
{
private readonly string funcName;
private readonly LuaTable self;
private readonly LuaFunction func;

public Handler(string funcName, LuaTable self = null)
{
this.funcName = funcName;
this.self = self;
}

public Handler(LuaFunction func)
{
this.func = func;
}

public void Handle(T1 a, T2 b)
{
if (this.func == null)
{
if (self == null) return;
if (!(self[funcName] is LuaFunction f)) return;
f.Call(self, a, b);
}
else
{
func.Call(a, b);
}
}
}
IL2CPP裁剪代码问题
  • 什么是代码裁剪](https://docs.unity3d.com/Manual/ManagedCodeStripping.html)) 当Unity中的Scripting Backend为IL2CPP时,默认会勾选代码裁剪,此时,构建时Unity代码裁剪工具会分析项目中的程序集,查找和删除未使用的代码,裁剪掉没有使用到的代码。
  • 带来问题 参考Unity官方回答) 问题主要出在MakeGenericType()方法上,该方法作用为:替代由当前泛型类型定义的类型参数组成的类型数组的元素,并返回表示结果构造类型的 Type 对象。目前IL2CPP还没有完全支持该方法,只能处理代码中已有的类型。
  • 解决方法 可以通过在代码中明确声明需要使用到的类型来规避这一问题。额外定义特性用于修饰委托。
1
2
3
4
5
6
7
8
9
10
[AttributeUsage(AttributeTargets.Delegate)]
public class NativeHandle : Attribute
{
public NativeHandle(Type type)
{
this.Type = type;
}

public Type Type { get; private set; } = null;
}
1
2
[NativeHandle(typeof(Handler<int>))]
public delegate void InitCompleteHandler(int retCode);