tolua源码分析(七)带out参数的C#函数

tolua源码分析(七)带out参数的C#函数

上一节我们提到了如何将lua函数绑定到C#的委托。这一节我们来看一下带有out参数的C#函数,在lua层是如何使用的。example 14中给了如下一段lua代码:

local box = UnityEngine.BoxCollider
                                                                
function TestPick(ray)                                                                  
    local _layer = 2 ^ LayerMask.NameToLayer('Default')                
    local time = os.clock()                                                                                              
    local flag, hit = UnityEngine.Physics.Raycast(ray, RaycastHit.out, 5000, _layer)                                
                    
    if flag then
        print('pick from lua, point: '..tostring(hit.point))                                        
    end
end

例子中调用的Raycast方法是带有out参数的,即

public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);

下面我们来深入源码一探究竟。首先在调用UnityEngine.BoxCollider时,会触发到C#层的LuaOpen_UnityEngine_BoxCollider函数,这个函数是在默认的LuaBinder.Bind函数中出现的:

L.BeginPreLoad();
L.AddPreLoad("UnityEngine.BoxCollider", LuaOpen_UnityEngine_BoxCollider, typeof(UnityEngine.BoxCollider));
...
L.EndPreLoad();

这里的AddPreLoad函数起到一个延迟注册的作用,在调用之后LuaOpen_UnityEngine_BoxCollider不会立刻触发,而是绑定到了lua层package.preload上,在lua层真正用到时,才会触发注册流程。回忆一下lua层访问C# namespace时,会触发module_index_event,如果下一步要访问的C# class不存在,则会往package.preload中查找,找到就发起require,lua的require会优先使用preload里自定义过的函数进行加载:

lua_getref(L, LUA_RIDX_PRELOAD);    //stack: t key space preload
lua_pushvalue(L, -2);
lua_pushstring(L, ".");
lua_pushvalue(L, 2);
lua_concat(L, 3);                   //stack: t key space preload key
lua_pushvalue(L, -1);               //stack: t key space preload key1 key1
lua_rawget(L, -3);                  //stack: t key space preload key1 value        

if (!lua_isnil(L, -1)) 
{      
    lua_pop(L, 1);                      //stack: t key space preload key1
    lua_getref(L, LUA_RIDX_REQUIRE);
    lua_pushvalue(L, -2);
    lua_call(L, 1, 1);                    
}

LuaOpen_UnityEngine_BoxCollider函数的核心就是调用BoxColliderWrap类的注册函数,这没什么好说的。

然后,我们注意到lua层的函数TestPick接收一个Ray参数,这个参数要从C#层传进去。而Ray类型是struct类型,我们不希望push到lua层产生任何的gc。来看看具体是怎么实现的:

public static void Push(IntPtr L, Ray ray)
{
    LuaStatic.GetPackRay(L);
    Push(L, ray.direction);
    Push(L, ray.origin);

    if (LuaDLL.lua_pcall(L, 2, 1, 0) != 0)
    {
        string error = LuaDLL.lua_tostring(L, -1);
        throw new LuaException(error);
    }
}

解决思路就是让lua层自己去构造lua层的ray,C#层只负责传数据,不再使用userdata。GetPackRay会将C#层之前缓存的lua函数Ray.New压入lua栈顶。这个函数接收两个lua vector3参数:

function Ray.New(direction, origin)
	local ray = {}	
	ray.direction 	= direction:Normalize()
	ray.origin 		= origin
	setmetatable(ray, Ray)	
	return ray
end

那么容易猜到的是,C#的vector3类型也会通过类似的方式传到lua层:

LUALIB_API void tolua_pushvec3(lua_State *L, float x, float y, float z)
{
	lua_getref(L, LUA_RIDX_PACKVEC3);
	lua_pushnumber(L, x);
	lua_pushnumber(L, y);
	lua_pushnumber(L, z);
	lua_call(L, 3, 1);
}

LUA_RIDX_PACKVEC3这个就是lua函数Vector3.New的reference。可以看出,一个C#的Ray其实被拆成了6个float,传到了lua层,然后lua层重新组装一遍,成为lua层的Ray。通过这样的方式,C#传递struct到lua层时,避免了gc开销。

接下来就是真正调用Raycast的时候了。问题的关键是C#层如何判断lua层传过来的参数类型,首先是Ray:

public bool CheckRay(IntPtr L, int pos)
{
    if (LuaDLL.lua_type(L, pos) == LuaTypes.LUA_TTABLE)
    {
        return LuaDLL.tolua_getvaluetype(L, pos) == LuaValueType.Ray;
    }

    return false;            
}

显然lua层的ray是不是ray这件事情,只有lua层自己知道,所以tolua_getvaluetype这个函数最终会调用到lua层的GetValueType

local function GetValueType()	
	local getmetatable = getmetatable
	local ValueType = ValueType

	return function(udata)
		local meta = getmetatable(udata)	

		if meta == nil then
			return 0
		end

		return ValueType[meta] or 0
	end
end

每个lua层自己实现的struct,都会设置metatable为自身,例如Ray:

UnityEngine.Ray = Ray
setmetatable(Ray, Ray)
return Ray

然后在tolua启动时,会把这些struct的lua都require进来,作为全局可访问的module table:

Mathf		= require "UnityEngine.Mathf"
Vector3 	= require "UnityEngine.Vector3"
Quaternion	= require "UnityEngine.Quaternion"
Vector2		= require "UnityEngine.Vector2"
Vector4		= require "UnityEngine.Vector4"
Color		= require "UnityEngine.Color"
Ray			= require "UnityEngine.Ray"
Bounds		= require "UnityEngine.Bounds"
RaycastHit	= require "UnityEngine.RaycastHit"
Touch		= require "UnityEngine.Touch"
LayerMask	= require "UnityEngine.LayerMask"
Plane		= require "UnityEngine.Plane"
Time		= reimport "UnityEngine.Time"

接着在ValueType这个table中,设置了这些module对应到C#的LuaValueType类中的index:

local ValueType = {}

ValueType[Vector3] 		= 1
ValueType[Quaternion]	= 2
ValueType[Vector2]		= 3
ValueType[Color]		= 4
ValueType[Vector4]		= 5
ValueType[Ray]			= 6
ValueType[Bounds]		= 7
ValueType[Touch]		= 8
ValueType[LayerMask]	= 9
ValueType[RaycastHit]	= 10
ValueType[int64]		= 11
ValueType[uint64]		= 12

C#中的相关定义如下:

public partial struct LuaValueType
{
    public const int None = 0;
    public const int Vector3 = 1;
    public const int Quaternion = 2;
    public const int Vector2 = 3;
    public const int Color = 4;
    public const int Vector4 = 5;
    public const int Ray = 6;
    public const int Bounds = 7;
    public const int Touch = 8;
    public const int LayerMask = 9;
    public const int RaycastHit = 10;
    public const int Int64 = 11;
    public const int UInt64 = 12;
    public const int Max = 64;
    ...
}

接着是第二个参数,RaycastHit.out,这个变量的注册是在C#的OpenLuaLibs中完成的,它把C#的函数绑定到了对应lua类的out字段上:

public static void OpenLuaLibs(IntPtr L)
{                        
    if (LuaDLL.tolua_openlualibs(L) != 0)
    {
        string error = LuaDLL.lua_tostring(L, -1);
        LuaDLL.lua_pop(L, 1);
        throw new LuaException(error);
    }

    SetOutMethods(L, "RaycastHit", GetOutRaycastHit);
    ...         
}

这个函数在调用时,会新建一个LuaOut类对象,将其push到lua层:

static int GetOutRaycastHit(IntPtr L)
{
    ToLua.PushOut(L, new LuaOut());
    return 1;
}

这个对象纯粹是起到一个类型检查的作用,但是每次调用时都会新建,是有gc开销的。在类型检查时,会根据lua层的userdata,转换到C#层的对应对象,判断是否为LuaOut类型:

static bool IsUserData(IntPtr L, int pos)
{
    object obj = null;
    int udata = LuaDLL.tolua_rawnetobj(L, pos);

    if (udata != -1)
    {
        ObjectTranslator translator = ObjectTranslator.Get(L);
        obj = translator.GetObject(udata);

        if (obj != null)
        {
            return obj is T;
        }
        else
        {
            return !IsValueType;
        }
    }

    return false;
}

第三个参数为5000,这没啥说的,直接判断是不是lua的number类型即可。那么还剩最后一个参数,这个参数其实也是number,它是由lua层的LayerMask.NameToLayer返回值决定的:

function LayerMask.NameToLayer(name)
	return Layer[name]
end

由于Layer本身是可以用户自定义的,因此需要先在C#层传递给lua层,这是在InitLayer这个函数中完成的:

static void InitLayer(IntPtr L)
{
    LuaDLL.tolua_createtable(L, "Layer");

    for (int i = 0; i < 32; i++)
    {
        string str = LayerMask.LayerToName(i);

        if (!string.IsNullOrEmpty(str))
        {
            LuaDLL.lua_pushstring(L, str);
            LuaDLL.lua_pushinteger(L, i);
            LuaDLL.lua_rawset(L, -3);
        }
    }

    LuaDLL.lua_pop(L, 1);
}

这个函数会把C#层定义的所有Layer传递给lua,在lua层新建一个Layer table,key为name,value为layer的值,这个值是一个int,所以最后一个参数的类型也是number。

4个参数的类型都检查完毕之后,我们来看下真正的执行逻辑:

UnityEngine.Ray arg0 = ToLua.ToRay(L, 1);
UnityEngine.RaycastHit arg1;
float arg2 = (float)LuaDLL.lua_tonumber(L, 3);
int arg3 = (int)LuaDLL.lua_tonumber(L, 4);
bool o = UnityEngine.Physics.Raycast(arg0, out arg1, arg2, arg3);
LuaDLL.lua_pushboolean(L, o);
if (o) ToLua.Push(L, arg1); else LuaDLL.lua_pushnil(L);
return 2;

之前我们提到过,把C#层的Ray push到lua层是不会产生gc的,那么反过来,从lua层取出C#的Ray是如何做到没有gc的呢?答案是类似的,C#层会调用lua层的函数,让lua层把Ray table展开为6个number放到栈上,C#层直接获取即可:

function Ray:Get()		
	local o = self.origin
	local d = self.direction
	return o.x, o.y, o.z, d.x, d.y, d.z
end
public static Ray ToRay(IntPtr L, int stackPos)
{
    int top = LuaDLL.lua_gettop(L);
    LuaStatic.GetUnpackRayRef(L);
    stackPos = LuaDLL.abs_index(L, stackPos);
    LuaDLL.lua_pushvalue(L, stackPos);

    if (LuaDLL.lua_pcall(L, 1, 6, 0) == 0)
    {            
        float ox = (float)LuaDLL.lua_tonumber(L, top + 1);
        float oy = (float)LuaDLL.lua_tonumber(L, top + 2);
        float oz = (float)LuaDLL.lua_tonumber(L, top + 3);
        float dx = (float)LuaDLL.lua_tonumber(L, top + 4);
        float dy = (float)LuaDLL.lua_tonumber(L, top + 5);
        float dz = (float)LuaDLL.lua_tonumber(L, top + 6);
        LuaDLL.lua_settop(L, top);
        return new Ray(new Vector3(ox, oy, oz), new Vector3(dx, dy, dz));
    }
    else
    {
        string error = LuaDLL.lua_tostring(L, -1);
        LuaDLL.lua_settop(L, top);
        throw new LuaException(error);
    }
}

如果Raycast返回了true,说明射线命中,out参数是有意义的,因此还需要将out参数push到lua层;如果返回false,push nil即可。

下一节我们将关注lua继承扩展C#类的机制。

如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

你可能感兴趣的:(tolua,tolua源码分析,c#,lua,unity)