Call custom c++ from Lua
cocos2d-x lua binds c++ class, class functions ,enum and some global functions to lua through auto-binding by bindings-generator(tools/bindings-generator) and some manual binding,so we can call custom c++ from lua conveniently.
Call the class member function
Open the lua-empty-test/src/hello.lua
file and we will find many function calls like cc.***
. They are actually calling the class member function.Let we see the initGLView
function.
local function initGLView()
local director = cc.Director:getInstance()
local glView = director:getOpenGLView()
if nil == glView then
glView = cc.GLViewImpl:create("Lua Empty Test")
director:setOpenGLView(glView)
end
director:setOpenGLView(glView)
glView:setDesignResolutionSize(480, 320, cc.ResolutionPolicy.NO_BORDER)
director:setDisplayStats(true)
director:setAnimationInterval(1.0 / 60)
end
The relationship between the lua function call and the c++ function call as follow:
| lua | c++ |
| cc.Director:getInstance() | cocos2d::Director::getInstance() |
| director:getOpenGLView() | director->getOpenGLView |
| cc.GLViewImpl:create("Lua Empty Test") | cocos2d::GLViewImpl::create("Lua Empty Test") |
| glView:setDesignResolutionSize(480, 320, cc.ResolutionPolicy.NO_BORDER) | glview->glView:setDesignResolutionSize(480, 320, ResolutionPolicy::NO_BORDER)|
From this table,we can see that the functions called in the lua are very similar with the functions called in the c++, and there are some key points that we need to pay attention to as follow:
cc
is a moudle name like namespace name in c++,it is cocos2d-x 3.0 new features.The relation between the lua modules and c++ namespaces as follow:
| Lua module name | c++ namespace |
| cc | cocos2d,cocos2d::extension,CocosDenshion,cocosbuilder |
| ccui | cocos2d::ui |
| ccs | cocostudio,cocostudio::timeline |
| sp | spine |
| ccexp | cocos2d::experimental,cocos2d::experimental::ui |
- No matter the functions are static or not in the c++, the functions called in the lua are all used
:
cc.ResolutionPolicy.NO_BORDER
corresponds toResolutionPolicy::NO_BORDER
which is enum value in the c++.Owing to faulty of current bindings-generator, enum values are bound to lua by manual.Different moudles have different lua file to keep the bindings value,the details as follows:
| moudle name | const value files |
| cc | Cocos2dConstants.lua, ExtensionConstants.lua, NetworkConstants.lua|
| ccui | GuiConstants.lua |
| ccs | StudioConstants.lua|
| ccexp | experimentalUIConstants.lua|
- For some functions, their parameters include cocos2d::Vec2, cocos2d::Vec3 ,and so on, we should do a conversion to call c++ function.For example:
void Node::setPosition(const Vec2& position)
In C++, we should call this function like this:
nodeObj->setPosition(Vec2(0.0f, 0.0f))
In lua, we should call this function like this:
nodeObj:setPosition(cc.p(0.0, 0.0))
cc.p(0.0, 0.0) is to construct an anonymous table like this {x = 0, y =0}
The other parametric types should be converted are listed as follow:
| parametric types | lua conversional format |
| cocos2d::Point | {x = numValue, y = numValue} |
| cocos2d::Vec3 | {x = numValue, y = numValue, z = numValue} |
| cocos2d::Vec4 | {x = numValue, y = numValue, z = numValue, w = numValue} |
| cocos2d::Rect | {x = numValue, y = numValue, width = numValue, height = numValue} |
| cocos2d::Size | {width = numValue, height = numValue} |
| cocos2d::Color4B | {r = numValue, g = numValue, b = numValue, a = numValue} |
| cocos2d::Color4F | {r = numValue, g = numValue, b = numValue, a = numValue} |
| cocos2d::Color3B | {r = numValue, g = numValue, b = numValue} |
| cocos2d::PhysicsMaterial | {density = numValue, restitution = numValue, friction = numValue} |
| cocos2d::AffineTransform | {a = numValue, b = numValue, c = numValue, d = numValue, tx = numValue, ty = numValue} |
| cocos2d::FontDefinition | {fontName = stringValue, fontSize = numValue, fontAlignmentH = numValue, fontAlignmentV = numValue, fontFillColor = {r = numValue, g = numValue, b = numValue}, fontDimensions = {width = numValue, height = numValue}, shadowEnabled = boolValue[,shadowOffset = {width = numValue, height = numValue}, shadowBlur = numValue, shadowOpacity = numValue], strokeEnabled = boolValue[,strokeColor = {r = numValue, g = numValue, b = numValue}, strokeSize = numValue]} |
| cocos2d::Vector | {objValue1,objValue2,...,objValuen,...}|
| cocos2d::Map<std::string, T>| {key1 = objValue1, key2 = objValue2,..., keyn = objValuen,...} |
| cocos2d::Value | {objValue1,objValue2,...,objValuen,...} or key1 = objValue1, key2 = objValue2,..., keyn = objValuen,...} or stringValue or boolValue or numValue |
| cocos2d::ValueMap | {key1 = Value1, key2 = Value2,..., keyn = Valuen,...} |
| cocos2d::ValueMapIntKey | {numKey1 = Value1, intKey2 = Value2, ...,intKeyn = Valuen,...} |
| cocos2d::ValueVector | {Value1, Value2, ..., Valuen, ...} |
| std::vector<string> | {stringValue1, stringValue2, ..., stringValuen, ...} |
| std::vector<int> | {numValue1, numValue2, ..., numValuen, ...} |
| std::vector<float> | {numValue1, numValue2, ..., numValuen, ...} |
| std::vector<unsigned short> | {numValue1, numValue2, ..., numValuen, ...} |
| cocos2d::Mat4 | {numValue1,numValue2,..., numValue16} |
| cocos2d::TTFConfig |{fontFilePath = stringValue, fontSize = numValue, glyphs = numValue, customGlyphs = stringValue, distanceFieldEnabled = boolValue, outlineSize = numValue}
| cocos2d::MeshVertexAttrib| {size = numValue, type = numValue, vertexAttrib = numValue, vertexAttrib =numValue} |
| cocos2d::BlendFunc | { src = numValue, dst = numValue} |
Call global functions
cocos2d-x v3 also binds some global functions to lua by manual, such as kmGLPushMatrix
, kmGLTranslatef
and kmGLLoadMatrix
etc.We can call these global function in the lua as follows:
kmGLPopMatrix()
Call OpenGL functions
cocos2d-x v3 binds some OpenGL functions to Lua.All the OpenGL functions are in the gl
moudle,and we can call OpenGL functions in the lua like this:
local glNode = gl.glNodeCreate()
glNode:setContentSize(cc.size(256, 256))
glNode:setAnchorPoint(cc.p(0.5, 0.5))
uniformCenter = gl.getUniformLocation(program,"center")
uniformResolution = gl.getUniformLocation( program, "resolution")
You can refer to lua-tests/DrawPrimitiveTest
and lua-tests/OpenGLTest
for more information.
Summa
From what has been discussed above, you can find that calling c++ functions from lua is conveniently, and you can refer to lua-tests
for more detail information.
Bind a c++ class to lua by bindings-generator automatically
Since cocos2d-x v3.0,we use bindings-generator to bind c++ class to lua automatically.
The bindings-generator is based on tolua++, you could config the ini file in the tools/tolua
directory and then run the genbindings.py script to generate the binding code. By using this method, it greatly reduce the configuration work of writing pkg files which used in cococ2d-x v2.
Next,let's take a custom class as an example to show how to use the bindings-generator to bind a c++ class to lua
Create a custom class
Here is the code snippets:
// CustomClass.h
#ifndef __CUSTOM__CLASS
#define __CUSTOM__CLASS
#include "cocos2d.h"
namespace cocos2d {
class CustomClass : public cocos2d::Ref
{
public:
CustomClass();
~CustomClass();
static cocos2d::CustomClass* create();
bool init();
CREATE_FUNC(CustomClass);
};
} //namespace cocos2d
#endif // __CUSTOM__CLASS
Note:
- We omit the cpp file because the bindings-generator only scan the header files
- We suggest that the custom class should be inheritent from cocos2d::Ref class,it is mainly due to the fact that the destructor of cocos2d::Ref would call
removeScriptObjectByObject
to reduce reference count of userdata which create in the c++ automaticly to avoid the memory leak.
Add a new cocos2dx_custom.ini file
Navigate to the tools/lua folder and create a new file named cocos2dx_custom.ini,the detail of cocos2dx_custom.ini is as follows:
[cocos2dx_custom]
# the prefix to be added to the generated functions. You might or might not use this in your own
# templates
prefix = cocos2dx_custom
# create a target namespace (in javascript, this would create some code like the equiv. to `ns = ns
# all classes will be embedded in that namespace
target_namespace = cc
android_headers = -I%(androidndkdir)s/platforms/android-14/arch-arm/usr/include -I%(androidndkdir)s/sources/cxx-stl/gnu-libstdc++/4.7/libs/armeabi-v7a/include -I%(androidndkdir)s/sources/cxx-stl/gnu-libstdc++/4.7/include -I%(androidndkdir)s/sources/cxx-stl/gnu-libstdc++/4.8/libs/armeabi-v7a/include -I%(androidndkdir)s/sources/cxx-stl/gnu-libstdc++/4.8/include
android_flags = -D_SIZE_T_DEFINED_
clang_headers = -I%(clangllvmdir)s/lib/clang/3.3/include
clang_flags = -nostdinc -x c++ -std=c++11 -U __SSE__
cocos_headers = -I%(cocosdir)s/cocos -I%(cocosdir)s/my -I%(cocosdir)s/cocos/2d -I%(cocosdir)s/cocos/base -I%(cocosdir)s/cocos/ui -I%(cocosdir)s/cocos/physics -I%(cocosdir)s/cocos/2d/platform -I%(cocosdir)s/cocos/2d/platform/android -I%(cocosdir)s/cocos/math/kazmath -I%(cocosdir)s/extensions -I%(cocosdir)s/external -I%(cocosdir)s/cocos/editor-support -I%(cocosdir)s
cocos_flags = -DANDROID -DCOCOS2D_JAVASCRIPT
cxxgenerator_headers =
# extra arguments for clang
extra_arguments = %(android_headers)s %(clang_headers)s %(cxxgenerator_headers)s %(cocos_headers)s %(android_flags)s %(clang_flags)s %(cocos_flags)s %(extra_flags)s
# what headers to parse
headers = %(cocosdir)s/cocos/my/CustomClass.h
# what classes to produce code for. You can use regular expressions here. When testing the regular
# expression, it will be enclosed in "^$", like this: "^Menu*$".
classes = CustomClass.*
# what should we skip? in the format ClassName::[function function]
# ClassName is a regular expression, but will be used like this: "^ClassName$" functions are also
# regular expressions, they will not be surrounded by "^$". If you want to skip a whole class, just
# add a single "*" as functions. See bellow for several examples. A special class name is "*", which
# will apply to all class names. This is a convenience wildcard to be able to skip similar named
# functions from all classes.
skip =
rename_functions =
rename_classes =
# for all class names, should we remove something when registering in the target VM?
remove_prefix =
# classes for which there will be no "parent" lookup
classes_have_no_parents =
# base classes which will be skipped when their sub-classes found them.
base_classes_to_skip =
# classes that create no constructor
# Set is special and we will use a hand-written constructor
abstract_classes =
# Determining whether to use script object(js object) to control the lifecycle of native(cpp) object or the other way around. Supported values are 'yes' or 'no'.
script_control_cpp = no
All of the config files under tools/tolua
are the same format. Here is the list which you should care when writing your own ini file:
- [title]: To config the title which will by used by the tools/tolua/gengindings.py scripts. Generally, the title could be the file name.
- prefix: To config the prefix of a function name, generally, we also use the file name as the prefix.
- target_namespace: To config the module name in the lua. Here we use the
cc
as the module name, when you want to useCustomClass
in the lua, you must put a prefix namedcc
in front of the name. For example, theCustomClass
could be reference ascc.CustomClass
. - headers: To config all the header files needed for parsing and the %(cocosdir)s is the engine root path of cocos2d-x.
- classes: To config all the classes needed to bind. Here it supports regular expression. So we could set MyCustomClass.* here. For looking more specified usage, you could ref to
tools/tolua/cocos2dx.ini
. -
skip: To config the functions needed to be omit. Now the bindings-generator can't parse
void*
type and also the delegate type, so these types needed to be bind manually. And at this circumstance, you should omit all these types first and then to bind them manually. You could ref to the config files under pathcocos/scripting/lua-bindings/auto
. -
rename_functions: To config the functions need to be renamed in the scripting layer. Due to some reasons, developers want more scripting friendly API, so the config option is for this purpose.
-
rename_classes: Not used any more.
-
remove_prefix: Not used any more.
-
classes_have_no_parents: To config the parent class needed to be filter. This option is seldom modified.
-
abstract_classes: To config the classes whose public constructor don't need to be exported.
-
script_control_cpp:yes. To config whether the scripting layer manage the object life time or not. If no, then the c++ layer cares about their life time. Now, it is imperfect to control native object's life time in scripting layer. So you could simply leave it to no.
Subclass Inherit from c++ Classes Bound to Lua
Sometimes, C++ classes's lua bindings aren't satisfied with our requirements,so we want to add some new function to extend the bindings like inheritance mechanism of C++. Through class(classname, super)
function in the cocos/scripting/lua-bindings/script/cocos2d/extern.lua
,we can realize this requirement easily.The details function are as follow:
function class(classname, super)
local superType = type(super)
local cls
if superType ~= "function" and superType ~= "table" then
superType = nil
super = nil
end
if superType == "function" or (super and super.__ctype == 1) then
-- inherited from native C++ Object
cls = {}
if superType == "table" then
-- copy fields from super
for k,v in pairs(super) do cls[k] = v end
cls.__create = super.__create
cls.super = super
else
cls.__create = super
end
cls.ctor = function() end
cls.__cname = classname
cls.__ctype = 1
function cls.new(...)
local instance = cls.__create(...)
-- copy fields from class to native object
for k,v in pairs(cls) do instance[k] = v end
instance.class = cls
instance:ctor(...)
return instance
end
else
-- inherited from Lua Object
if super then
cls = clone(super)
cls.super = super
else
cls = {ctor = function() end}
end
cls.__cname = classname
cls.__ctype = 2 -- lua
cls.__index = cls
function cls.new(...)
local instance = setmetatable({}, cls)
instance.class = cls
instance:ctor(...)
return instance
end
end
return cls
end
Through this function,we can realize the inheritance mechanism easily.For example,if we want to derive from cc.Node
,the steps we should do are as follow
- Define a subclass by
class
function
local SubNode = class("SubNode",function()
return cc.Node:create()
end)
--This functin like the construtor of c++ class
function SubNode:ctor()
-- do initialized
end
function SubNode:addSprite(filePath)
local sprite = cc.Sprite:create(filePath)
sprite:setPosition(cc.p(0, 0))
self:addChild(sprite)
end
...
- Create a object of subclass and use it
local node = SubNode.new()
node:addSprite("xxx.jpg")
Note:
new
is implemented defautly in theclass
function.Because the type of second parameter ofclass
we passed isfunction
,so when we callnew
, the final realization is as follow:
funtion SubNode.new(...)
local instance = cc.Node:create()
-- copy fields from SubNode to native object
for k,v in pairs(SubNode) do instance[k] = v end
instance.class = SubNode
instance:ctor(...)
return instance
end
So, the object which created by new
have all the property and behavior of the cc.Node
.In the meanwhile,it also have the property of SubNode
.
- Because
SubNode
is derived fromcc.Node
, so it also can override the functions ofcc.Node
. For example:
function SubNode:setPostion(x,y)
print(string.format("x = %0.2f, y = %0.2f"), x, y)
end
If we still want to call the function of the same name of super class, you can call it as follows:
getmetatable(SubNode):setPosition(x, y)
- The override functions of inherited class in the lua can't be called in the c++.
Memory Management
Cocos2d-x v3.x use memory management and garbage collection of lua itself,except the release of userdata
.If the corresponding classes are derived from Ref
class, the release of userdata
is managed in the c++ by control the two table in the register table named toluafix_refid_ptr_mapping
and tolua_value_root
.
We will give a simple but typical test case to explain this mechanism through modifying the lua-empty-test\hello.lua
.
Simple Test Case
- Creat a sprite in the head of
createDog
function
local testSprite = cc.Sprite:create("res/land.png")
- Then called this sprite in the
tick
function as follow:
testSprite:getPosition()
- After a period of time, we will see the error message as follows:
cocos2d: [LUA-print] stack traceback:
[string "src/hello.lua"]:13: in function <[string "src/hello.lua"]:10>
[C]: in function 'getPosition'
[string "src/hello.lua"]:98: in function <[string "src/hello.lua"]:88>
cocos2d: [LUA-print] ----------------------------------------
cocos2d: [LUA-print] ----------------------------------------
cocos2d: [LUA-print] LUA ERROR: [string "src/hello.lua"]:98: invalid 'self' in function 'tolua_cocos2d_Node_getPosition'
Error Analysis
This error triggered is because the testsprite didn't add to any other node as a child after created,so the corresponding c++ object was released at the end of a frame.
For the details of memory management from create to release, We should analyze from several aspects as follows:
Memory Management for Class Object
The Class Memebers of Ref Class for Memory Management
In the CCRef.h
, we could see the usage of CC_ENABLE_SCRIPT_BINDING
.
#if CC_ENABLE_SCRIPT_BINDING
public:
/// object id, ScriptSupport need public _ID
unsigned int _ID;
/// Lua reference id
int _luaID;
/// scriptObject, support for swift
void* _scriptObject;
#endif
We should notice the _ID
and _luaID
,these two class members is very important when push a Ref object to lua by calling toluafix_pushusertype_ccobject
to store a key-value table named toluafix_refid_ptr_mapping
in the registry.the _ID
is key and the related c++ object pointer is value. The related code fragment in the toluafix_pushusertype_ccobject
is as follows:
//Extract from `toluafix_pushusertype_ccobject` in the tolua_fix.cpp
lua_pushstring(L, TOLUA_REFID_PTR_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: refid_ptr */
lua_pushinteger(L, refid); /* stack: refid_ptr refid */
lua_pushlightuserdata(L, vPtr); /* stack: refid_ptr refid ptr */
lua_rawset(L, -3); /* refid_ptr[refid] = ptr, stack: refid_ptr */
lua_pop(L, 1); /* stack: - */
Note:
TOLUA_REFID_PTR_MAPPING
is macro definition represent for "toluafix_refid_ptr_mapping"LUA_REGISTRYINDEX
is definition ofPseudo-Index
for registry of Luarefid
is value of_ID
vPtr
is value of related c++ object pointer
Create a Ref object from Lua
Like the upper test,when we called cc.Sprite:create("res/land.png")
in the lua,the related c++ processing flow is as follow:
- Call the
cocos2d::Sprite::create("res/land.png")
by the lua bindings to create a Sprite object and push it into the lua stack,the related codes are as follows:
//Extract from `lua_cocos2dx_Sprite_create` in the lua_cocos2dx_auto.cpp
std::string arg0;
ok &= luaval_to_std_string(tolua_S, 2,&arg0, "cc.Sprite:create");
if (!ok) { break; }
cocos2d::Sprite* ret = cocos2d::Sprite::create(arg0);
object_to_luaval<cocos2d::Sprite>(tolua_S, "cc.Sprite",(cocos2d::Sprite*)ret);
return 1;
- Call
toluafix_pushusertype_ccobject
when push created object to lua stack
//Extract from `object_to_luaval` in the LuaBasicConversions.h
if (std::is_base_of<cocos2d::Ref, T>::value)
{
// use c style cast, T may not polymorphic
cocos2d::Ref* dynObject = (cocos2d::Ref*)(ret);
int ID = (int)(dynObject->_ID) ;
int* luaID = &(dynObject->_luaID);
toluafix_pushusertype_ccobject(L,ID, luaID, (void*)ret,type);
}
In the toluafix_pushusertype_ccobject
,we will use two tables named "toluafix_refid_ptr_mapping" and "toluafix_refid_type_mapping" in the lua's registry to store the two key-value pairs about _ID
-object pointer
and _ID
-object type name
.The details are as follow:
//Extract from `toluafix_pushusertype_ccobject` in the tolua_fix.cpp
if (*p_refid == 0)
{
*p_refid = refid;
lua_pushstring(L, TOLUA_REFID_PTR_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: refid_ptr */
lua_pushinteger(L, refid); /* stack: refid_ptr refid */
lua_pushlightuserdata(L, vPtr); /* stack: refid_ptr refid ptr */
lua_rawset(L, -3); /* refid_ptr[refid] = ptr, stack: refid_ptr */
lua_pop(L, 1); /* stack: - */
lua_pushstring(L, TOLUA_REFID_TYPE_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: refid_type */
lua_pushinteger(L, refid); /* stack: refid_type refid */
lua_pushstring(L, vType); /* stack: refid_type refid type */
lua_rawset(L, -3); /* refid_type[refid] = type, stack: refid_type */
lua_pop(L, 1); /* stack: - */
//printf("[LUA] push CCObject OK - refid: %d, ptr: %x, type: %s\n", *p_refid, (int)ptr, type);
}
- Call
tolua_pushusertype_internal
to determine whether to create a new userdata, or just update the userdata.
void tolua_pushusertype_internal (lua_State* L, void* value, const char* type, int addToRoot)
{
if (value == NULL)
lua_pushnil(L);
else
{
luaL_getmetatable(L, type); /* stack: mt */
if (lua_isnil(L, -1)) { /* NOT FOUND metatable */
lua_pop(L, 1);
return;
}
lua_pushstring(L,"tolua_ubox");
lua_rawget(L,-2); /* stack: mt ubox */
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushstring(L, "tolua_ubox");
lua_rawget(L, LUA_REGISTRYINDEX);
};
lua_pushlightuserdata(L,value); /* stack: mt ubox key<value> */
lua_rawget(L,-2); /* stack: mt ubox ubox[value] */
if (lua_isnil(L,-1))
{
lua_pop(L,1); /* stack: mt ubox */
lua_pushlightuserdata(L,value);
*(void**)lua_newuserdata(L,sizeof(void *)) = value; /* stack: mt ubox value newud */
lua_pushvalue(L,-1); /* stack: mt ubox value newud newud */
lua_insert(L,-4); /* stack: mt newud ubox value newud */
lua_rawset(L,-3); /* ubox[value] = newud, stack: mt newud ubox */
lua_pop(L,1); /* stack: mt newud */
/*luaL_getmetatable(L,type);*/
lua_pushvalue(L, -2); /* stack: mt newud mt */
lua_setmetatable(L,-2); /* update mt, stack: mt newud */
#ifdef LUA_VERSION_NUM
lua_pushvalue(L, TOLUA_NOPEER); /* stack: mt newud peer */
lua_setfenv(L, -2); /* stack: mt newud */
#endif
}
else
{
/* check the need of updating the metatable to a more specialized class */
lua_insert(L,-2); /* stack: mt ubox[u] ubox */
lua_pop(L,1); /* stack: mt ubox[u] */
lua_pushstring(L,"tolua_super");
lua_rawget(L,LUA_REGISTRYINDEX); /* stack: mt ubox[u] super */
lua_getmetatable(L,-2); /* stack: mt ubox[u] super mt */
lua_rawget(L,-2); /* stack: mt ubox[u] super super[mt] */
if (lua_istable(L,-1))
{
lua_pushstring(L,type); /* stack: mt ubox[u] super super[mt] type */
lua_rawget(L,-2); /* stack: mt ubox[u] super super[mt] flag */
if (lua_toboolean(L,-1) == 1) /* if true */
{
lua_pop(L,3); /* mt ubox[u]*/
lua_remove(L, -2);
return;
}
}
/* type represents a more specilized type */
/*luaL_getmetatable(L,type); // stack: mt ubox[u] super super[mt] flag mt */
lua_pushvalue(L, -5); /* stack: mt ubox[u] super super[mt] flag mt */
lua_setmetatable(L,-5); /* stack: mt ubox[u] super super[mt] flag */
lua_pop(L,3); /* stack: mt ubox[u] */
}
lua_remove(L, -2); /* stack: ubox[u]*/
if (0 != addToRoot)
{
lua_pushvalue(L, -1);
tolua_add_value_to_root(L, value);
}
}
}
We use a table named ubox
to store key-value pairs about userdata
and objec pointer
.This table would be used when the destruction of the object.
- Call
tolua_add_value_to_root
to add a reference count foruserdata
in the lua by thetolua_value_root
table in the lua registry.The mechanism will make the object in the lua wouldn't collected by lua gc.For example:
local node = cc.Node:create()
node.extendValue = 10000
nodeParent:addChild(node, 0 , 9999)
These codes create a node object and extend the attribute of the node object dynamically by the lua's feature.Whenwe want to get this node and its extended attribute somewhere, we can do as follow:
local child = lnodeParent:getChildByTag(9999)
print(child.extendValue)
If we don't call the tolua_add_value_to_root
,the result of print(child.extendValue)
would be uncertain.sometimes the result would be 10000,and sometimes it would be nil
.Because we wouldn't controll the lua automatic gc effectively,when lua gc think there is no other place have the references for this userdata,it will collect this userdata.When we call getChildByTag to get a node object,it would be create a new userdata and the extend attribute would disapper certainly. So,we add a reference count for the userdata tolua_value_root
table in the lua registry in the c++ to avoid generating this error.
The Release of the Userdata
When calling the desturctor of Ref, it would trigger the release of the userdata.
In the destructor of Ref, we could see the following codes:
#if CC_ENABLE_SCRIPT_BINDING
// if the object is referenced by Lua engine, remove it
if (_luaID)
{
ScriptEngineManager::getInstance()->getScriptEngine()->removeScriptObjectByObject(this);
}
...
#endif
Note:After we push a c++ object to lua,the related _luaID would be not 0,so we can call removeScriptObjectByObject
The removeScriptObjectByObject
called would trigger the call of toluafix_remove_ccobject_by_refid
, and this function would call some lua c APIs to operate the table like toluafix_refid_ptr_mapping
、toluafix_refid_type_mapping
and tolua_value_root
table in the registry.
The specific implementation of toluafix_remove_ccobject_by_refid
is as follows:
TOLUA_API int toluafix_remove_ccobject_by_refid(lua_State* L, int refid)
{
void* ptr = NULL;
const char* type = NULL;
void** ud = NULL;
if (refid == 0) return -1;
// get ptr from tolua_refid_ptr_mapping
lua_pushstring(L, TOLUA_REFID_PTR_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: refid_ptr */
lua_pushinteger(L, refid); /* stack: refid_ptr refid */
lua_rawget(L, -2); /* stack: refid_ptr ptr */
ptr = lua_touserdata(L, -1);
lua_pop(L, 1); /* stack: refid_ptr */
if (ptr == NULL)
{
lua_pop(L, 1);
// Lua stack has closed, C++ object not in Lua.
// printf("[LUA ERROR] remove CCObject with NULL ptr, refid: %d\n", refid);
return -2;
}
// remove ptr from tolua_refid_ptr_mapping
lua_pushinteger(L, refid); /* stack: refid_ptr refid */
lua_pushnil(L); /* stack: refid_ptr refid nil */
lua_rawset(L, -3); /* delete refid_ptr[refid], stack: refid_ptr */
lua_pop(L, 1); /* stack: - */
// get type from tolua_refid_type_mapping
lua_pushstring(L, TOLUA_REFID_TYPE_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: refid_type */
lua_pushinteger(L, refid); /* stack: refid_type refid */
lua_rawget(L, -2); /* stack: refid_type type */
if (lua_isnil(L, -1))
{
lua_pop(L, 2);
printf("[LUA ERROR] remove CCObject with NULL type, refid: %d, ptr: %p\n", refid, ptr);
return -1;
}
type = lua_tostring(L, -1);
lua_pop(L, 1); /* stack: refid_type */
// remove type from tolua_refid_type_mapping
lua_pushinteger(L, refid); /* stack: refid_type refid */
lua_pushnil(L); /* stack: refid_type refid nil */
lua_rawset(L, -3); /* delete refid_type[refid], stack: refid_type */
lua_pop(L, 1); /* stack: - */
// get ubox
luaL_getmetatable(L, type); /* stack: mt */
lua_pushstring(L, "tolua_ubox"); /* stack: mt key */
lua_rawget(L, -2); /* stack: mt ubox */
if (lua_isnil(L, -1))
{
// use global ubox
lua_pop(L, 1); /* stack: mt */
lua_pushstring(L, "tolua_ubox"); /* stack: mt key */
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: mt ubox */
};
// cleanup root
tolua_remove_value_from_root(L, ptr);
lua_pushlightuserdata(L, ptr); /* stack: mt ubox ptr */
lua_rawget(L,-2); /* stack: mt ubox ud */
if (lua_isnil(L, -1))
{
// Lua object has released (GC), C++ object not in ubox.
//printf("[LUA ERROR] remove CCObject with NULL ubox, refid: %d, ptr: %x, type: %s\n", refid, (int)ptr, type);
lua_pop(L, 3);
return -3;
}
// cleanup peertable
lua_pushvalue(L, LUA_REGISTRYINDEX);
lua_setfenv(L, -2);
ud = (void**)lua_touserdata(L, -1);
lua_pop(L, 1); /* stack: mt ubox */
if (ud == NULL)
{
printf("[LUA ERROR] remove CCObject with NULL userdata, refid: %d, ptr: %p, type: %s\n", refid, ptr, type);
lua_pop(L, 2);
return -1;
}
// clean userdata
*ud = NULL;
lua_pushlightuserdata(L, ptr); /* stack: mt ubox ptr */
lua_pushnil(L); /* stack: mt ubox ptr nil */
lua_rawset(L, -3); /* ubox[ptr] = nil, stack: mt ubox */
lua_pop(L, 2);
//printf("[LUA] remove CCObject, refid: %d, ptr: %x, type: %s\n", refid, (int)ptr, type);
return 0;
}
The steps are as follows:
-
Get related object pointer stored in the
toluafix_refid_ptr_mapping
table by the value of_luaID
,then store it -
Remove reference relationship of the object pointer from
toluafix_refid_ptr_mapping
table by_luID
-
Get related type name stored in the
tolua_refid_type_mapping
table by the value of_luaID
,then store it -
Remove reference relationship of type name from
tolua_refid_type_mapping
table by_luID
-
Get the related metatable by the type name
-
Get the
ubox
table -
Remove reference relationship of userdata from
tolua_value_root
table by the object pointer got in the upper step -
Clean userdata and remove reference relationship of uesrdata from
ubox
by the object pointer got in the upper step.Note:To destroy a object cited by lua, we only called '*ud = NULL;'
Through the above steps,the refernce relationships in the toluafix_refid_ptr_mapping
、tolua_refid_type_mapping
and tolua_refid_type_mapping
table in the registry would be removed, release the userdata
which is created when push c++ object to lua stack, and when lua gc trigger, the related object would be collected if there is no other place refer to it.
Memory Management for Lua Callback Function
Cocos2dx have been used toluafix_refid_function_mapping
table in the registry to manage the gc of lua callback function
Add a refrence for Lua Callback Function
When we define a lua function which would be called throuch c++ codes, we whould store the pointer of this function in the toluafix_refid_function_mapping
table by calling toluafix_ref_function
function in the tolua_fix.cpp
.Cocos2d-x bound a series of functions like registerScriptHandler
and addEventListener
to finish this work.
Let's use registerScriptHandler
of Node
as a sample,we could use it as follows in lua:
local function onNodeEvent(event)
if "enter" == event then
--do something
end
end
nodeObject:registerScriptHandler(onNodeEvent)
The related bindings funtion is named tolua_cocos2d_Node_registerScriptHandler
in the lua_cocos2dx_manual.cpp
,the most important sections are as follows:
LUA_FUNCTION handler = toluafix_ref_function(tolua_S,2,0);
ScriptHandlerMgr::getInstance()->addObjectHandler((void*)self, handler, ScriptHandlerMgr::HandlerType::NODE);
toluafix_ref_function
is implemented to store the related function pointer intotoluafix_refid_function_mapping
table in the registry with a staic variable nameds_function_ref_id
.This operation makes the lua function avoid being collected by lua gc because thattoluafix_refid_function_mapping
table have a refrence of this function. The details are as follow:
TOLUA_API int toluafix_ref_function(lua_State* L, int lo, int def)
{
// function at lo
if (!lua_isfunction(L, lo)) return 0;
s_function_ref_id++;
lua_pushstring(L, TOLUA_REFID_FUNCTION_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: fun ... refid_fun */
lua_pushinteger(L, s_function_ref_id); /* stack: fun ... refid_fun refid */
lua_pushvalue(L, lo); /* stack: fun ... refid_fun refid fun */
lua_rawset(L, -3); /* refid_fun[refid] = fun, stack: fun ... refid_ptr */
lua_pop(L, 1); /* stack: fun ... */
return s_function_ref_id;
}
addObjectHandler
is used to stored the map of object pointer and pair of s_function_ref_id and handler type.
Remove a refrence for Lua Callback Function
If lua callback funcion become useless, we should remove the refrence in the toluafix_refid_function_mapping
table in the registry. Cocos2d-x provided the toluafix_remove_function_by_refid
function to realize it.This function could be called by removeScriptHandler
of LuaStack
、removeScriptHandler
of LuaEngine
or directly.The details are as follows:
TOLUA_API void toluafix_remove_function_by_refid(lua_State* L, int refid)
{
lua_pushstring(L, TOLUA_REFID_FUNCTION_MAPPING);
lua_rawget(L, LUA_REGISTRYINDEX); /* stack: ... refid_fun */
lua_pushinteger(L, refid); /* stack: ... refid_fun refid */
lua_pushnil(L); /* stack: ... refid_fun refid nil */
lua_rawset(L, -3); /* refid_fun[refid] = nil, stack: ... refid_fun */
lua_pop(L, 1); /* stack: ... */
}
Note:
refid
is the corresponding value ofs_function_ref_id
.- For Ref object,we would call
ScriptHandlerMgr::getInstance()->removeObjectAllHandlers
to remove all the refrence funtion relationship which added by theScriptHandlerMgr::getInstance()->addObjectHandler
automaticly - Because Cocos2d-x v3.x support the features of c++ 11,so we can call the related remove function through the lambda function.For example:
//Extract from `lua_cocos2dx_TextureCache_addImageAsync` in the lua_cocos2dx_manual.cpp
LUA_FUNCTION handler = ( toluafix_ref_function(tolua_S, 3, 0));
self->addImageAsync(configFilePath, [=](Texture2D* tex){
int ID = (tex) ? (int)tex->_ID : -1;
int* luaID = (tex) ? &tex->_luaID : nullptr;
toluafix_pushusertype_ccobject(tolua_S, ID, luaID, (void*)tex, "cc.Texture2D");
LuaEngine::getInstance()->getLuaStack()->executeFunctionByHandler(handler,1);
LuaEngine::getInstance()->removeScriptHandler(handler);
});
By the mechanism of the lambda, we could get the value of handler which represents the corresponding value of s_function_ref_id
,so when we finish calling the lua callback function,we could call LuaEngine::getInstance()->removeScriptHandler(handler)
directly to remove the refrence of the lua callback function.
Summa
In this section, we explain that how to process some refrence tables in the c++ to manager the release of memory.These mechanisms don't break the orgin mechanism of lua gc.
Use Cocos Code IDE to Debug a Lua Game
Cocos Code IDE is tool that can debug a lua script,it has windows and mac version.you can debug Windows and Anroid lua games through the windows version, and you can debug Mac, iOS and android lua games through the Mac version. Now we will demonstrate how to use Cocos Code IDE to debug a lua game based on the mac version,the process of the windows version is almost the same.
Prerequisite
If you have been not installed the Cocos Code IDE,you can refer to Cocos Code IDE Installation.
Cocos Code IDE Configuration
Basic Settings
Click Cocos Code IDE/Preferences
to open the congiuration dialog,then select the Cocos/Lua
to set the directory of cocos2d-x v3.x in the Lua Frameworks
:
Additional Settings
You should set directory of some compliling tools about android if you need to replace the Android runtime which Cocos Code IDE provided.Click Cocos Code IDE/Preferences
then pitch on Cocos
to configurate the directory of related tools:
Debug a Lua Game
- Create a new Cococs Lua Project by the right click menu in the
Lua Projects Explorer
- Select
src/GameScene.lua
and open it,then toggle breakpoint by right click menu or double click
3.Click debug button on toolbar
4.Trigger the breakpoint,select "Yes" to open Debug Perspective
,and you will find many useful debug views like Call stacks
, Variables
and Breakpoints
,etc.
5.Use Step over
, Step into
, Step out
in the tool bar to debug
Code Hot Updating when Debugging
We could realize the hot updating of lua code when debugging by the Cocos Code IDE.
If you want to change the moving path of dog int the src/GameScene.lua, you can modify the "tick()" funcion to control the dog's position
local function tick()
if spriteDog.isPaused then return end
local x, y = spriteDog:getPosition()
if x > self.origin.x + self.visibleSize.width then
x = self.origin.x
else
x = x + 1
end
spriteDog:setPositionX(x)
end
Modify the implementation of function, for example, change the value 1 to 10 and save your change. Then you will find that you have improved the speed of SpriteDog without restarting the app!
How to Debug on the Other Target Platforms
The above example is executed on the Mac platform because of the default configuration of Cocos Code IDE.If you debug on the other target platforms,you should modify Debug Configurations
.
- Click
Debug Configurations
button on the toolbar to openDebug Configurations
dialog
Debug on the iOS Simulator
-
Check iOS Simulator radio button
-
Choose a runtime app
-
Click the Debug button,IDE will auto-install chosen runtime app and start runtime to debug
Debug on the iOS Device
1.You need a runtime IPA, you can build a custom runtime IPA by Cocos Code IDE, then install runtime IPA to iOS device.
-
Click
Build Runtime
on the toolbars -
Click
Yes
button on the pop-upCocos
dialog -
Click
Generate
button to GenerateCreate Native Source Wizard
-
Click
Close
button to finishCreate Native Source Wizard
-
Click
Build Runtime
on the toolbars to openRuntime Builder Wizard
dialog -
Check
Build iOS Device Runtime
and clickGenerate
button to generate -
Click
Close
button when Finished dialog pop up
- Click
Debug Configuration
,then checkRemote Debug
radio button on theDebug Configuration
dialog - Select
iOS
platform - Fill IP address of your device into the
Target IP
and Fill the IP address that your PC used on theHost IP
(Make sure that theTarget IP
andHost IP
can access each other) -
Click 'Debug' button to begin to debug
Debug on the Android Device by ADB Mode
- Prebuild Runtimelua.apk by
Build Runtime
like first ofDebug on the iOS Device
- Check
Android ADB Mode
radio button - Choose a runtime apk
- Click the
Debug
button - IDE will auto-install the chosen runtime apk and start to debug
Debug on the Android Deveice by WLAN
- Install runtime apk to your device manually. It is placed in CocosLuaGame/runtime/android.
- Start runtime on device manually
- Click
Debug Configuration
,then checkRemote Debug
radio button on theDebug Configuration
dialog - Fill IP address of your device into the
Target IP
and Fill the IP address that your PC used on theHost IP
(Make sure that theTarget IP
andHost IP
can access each other) - Click 'Debug' button to begin to debug