topameng / tolua

The fastest unity lua binding solution
MIT License
3.01k stars 874 forks source link

ToLua OpenLibs semantics mistake #168

Closed starwing closed 5 years ago

starwing commented 5 years ago

https://github.com/topameng/tolua/blob/197e3a0278c9aa110280f43a11ad1c0e45dd8cfc/Assets/ToLua/Core/LuaState.cs#L242

In Lua C API, a luaopen_ function need not direct register the routines with luaL_register(L, name, libs); (instead, it's not available in Lua >= 5.2). But in quoted line, the OpenLibs routine simply calls open function, it's at least two misunderstand about Lua C API:

  1. The open function could not directly be called, one should use lua_call to call it.
  2. It's caller's responsibility to register the returned value into LOADED table.

This is a good implement about "require a C module in C":


I raise this issue because this feature makes lua-protobuf failed to register into tolua. because lua-protobuf never calls luaL_register with a name to respect the Lua5.2+ semantics of module require.

topameng commented 5 years ago

luacall 跟直接调用一个luaopen 函数没有什么区别吧?如果只是newtable形式,可以参考 protected void OpenCJson() 需要自己去往 loaded里面注册。而不是简单调用一个luaopen 函数

topameng commented 5 years ago

OpenLibs 目的仅仅是调用 luaopen_ 函数并传入luaState L。并不负责注册_LOADED之类

starwing commented 5 years ago

luaopen_函数本身不一定只返回一个值,也不一定只往栈上推一个值。比如说lua-protobuf的luaopen_pb,首先新建了一个metatable表,然后创建库表,最后return 1表示只愿意返回最后的库表。如果直接call那栈就乱了。luaopen_函数本身是提供给 Lua 去调用的,不保证栈平衡。

ToLua为了方便C库的载入,应该还是要提供一个通用的“注册进_LOADED"的函数的。

topameng commented 5 years ago

但这种跟5.1.5 意图不一致,如果非5.1.5。本意是自己去写就像 OpenLuaSocket 和 OpenCJson 那样。毕竟有时候在_LOADED表里设置的名字之类是特殊的。我想大概是如下代码 luaState.LuaGetField(LuaIndexes.LUA_REGISTRYINDEX, "_LOADED"); lua.OpenLibs(LuaDLL.luaopen_pb); luaState.LuaSetField(-2, "pb"); lua.OpenLibs(LuaDLL.luaopen_pb_io); luaState.LuaSetField(-2, "pb.io");
lua.OpenLibs(LuaDLL.luaopen_pb_conv); luaState.LuaSetField(-2, "pb.conv");
lua.OpenLibs(LuaDLL.luaopen_pb_buffer); luaState.LuaSetField(-2, "pb.buffer");
lua.OpenLibs(LuaDLL.luaopen_pb_slice); luaState.LuaSetField(-2, "pb.slice");

topameng commented 5 years ago

也有些可能只是放入到 preload 表,可参考 OpenLuaSocket, 但这些都需要库提供一个示例给开发者调用。包括注册名字等都不是固定的。这边暂时只提供了最基本的函数封装包括 luaState.BeginPreLoad() 之类

starwing commented 5 years ago

那现在有个问题,这样,将lua-protobuf集成进tolua就需要额外的代码了。这个库在Lua5.1+, LuaJIT 2.0+ 都是编译后开箱即用的。但是集成进tolua就需要额外代码,这样不太合理吧?另外,退一步说,这样也就需要一个tolua的fork来做这个事情了,而且要两边同步,这样也不方便呀。对于这个事情您有什么好的办法吗?

topameng commented 5 years ago

直接能用需要自己提供类似 luaL_openlibs 这样的函数吧。它其实完成了lua基本库的注册。但不透露细节。也有些库如struct 确实扔给调用者去解决了,但如果这样光有一个函数,做为上层其实是无法直接注册的,因为无法假定名字,即使有类似luaL_openlibs函数,还是需要开发者设定名字和对应函数,其实跟上面OpenCJson差不太多。 对于5.1.5 LUALIB_API void luaL_openlibs (lua_State L) { const luaL_Reg lib = lualibs; for (; lib->func; lib++) { lua_pushcfunction(L, lib->func); lua_pushstring(L, lib->name); lua_call(L, 1, 0); } } 对于lua 5.3 LUALIB_API void luaL_openlibs (lua_State L) { const luaL_Reg lib; / "require" functions from 'loadedlibs' and set results to global table / for (lib = loadedlibs; lib->func; lib++) { luaL_requiref(L, lib->name, lib->func, 1); lua_pop(L, 1); / remove lib / } }

starwing commented 5 years ago

现在问题就是tolua应该提供但是没提供luaL_requiref这个函数。对5.1来说,直接lua_call就可以了,但是对于5.2+就需要luaL_requiref(实际上5.1也有,功能是做在luaL_register里的)。现在Lua的规范(从5.1开始)就是这种事情是Lua自己负责(要么是luaL_函数,要么是Lua自己内部处理),那么为了兼容其他Lua运行时,Lua C Module就没办法自己做这件事。但是tolua又不提供类似功能,就没人做这个事情了。只能集成lua-protobuf的人去做。但是这种集成只能是个fork,这就有同步问题了。只需要tolua提供一个类似luaL_requiref功能的接口,那么集成其他任何的通用C库(兼容5.1+)就会非常简单。连带集成cJSON的代码都能简化。

如果实在不愿意这么做,那也应该在tolua的C#代码里提供一个注册数组,放LuaCSFunuction和name,然后做个循环统一注册,这样就不需要每次重复push _LOADED再pop了(虽然require其实就一次,每次上来push再pop也不怎么耗费性能)

topameng commented 5 years ago

最好是库处理好,就像lua的openlibs,因为注册名字是固定的,留给上层设置,如果设置不一致,又会有应用问题。

topameng commented 5 years ago

tolua 提供的函数写起来就类似opencjson了,关键不应该使用者设置库名字,这样很容易lua脚本不一致,比如我起名pb3,require写起来都不一样,反倒lua c module很容易处理,导出一个统一的openpblibs函数完成注册。这样其他人写lua脚本库名字也是一致的。使用者也不用检查兼容不同luavm版本,使用不同注册方式。总的来说,直接调用一个函数成本最低,而且不关心内部细节

starwing commented 5 years ago

首先这个事儿根本就不是库去处理的。这是Lua,不是Python或者Rust或者Elixir。Lua的模块哲学向来就是你提供require的参数,至于变量名你爱用啥用啥的:

local my_pb_module = require "pb"
my_pb_module:loadfile "msg.pb"

然后,这里有三方:Lua语言框架、C库、Lua脚本开发者。一个库使用的时候的名字,是脚本开发者去local定义的;一个库用来require的名字,是C库作者去定义的(比如我的luaopen_pb),而Lua负责利用脚本开发者传递进来的C库作者规定的名字,通过require载入这个模块。这个操作是Lua需要考虑的。在这里,就应该是tolua去做的(放进_LOADED里)。事实上,这是ll_require这个Lua标准库函数做的事情。既然准备替换掉Lua自有的注册流程,你至少就得有个一致的东西去代替,而不是说这是别人的责任然后让C库作者去做——因为在其他的任何平台,作者都不需要做这件事。

退一万步,我也没办法在pb.c这个文件里去写注册的事情。按照你的做法,你依然是在LuaClient.cs这个文件里去做这件事的。所以你的做法反而就不简单。你没办法直接调用一个cJSON的函数去搞定这个,而是不得不为它去写一份代码。随着兼容Lua5.3的C库越来越多,越来越多的库都需要你去“额外写代码”。那么提供一个统一的,在C#层的注册表,模拟一下luaL_Reg,提供名字和函数,然后统一注册进_LOADED甚至只需要注册进_PRELOAD,这样很难么?

NewbieGameCoder commented 5 years ago

我插一句,讨论归讨论,不要动不动上升到责任划分的程度哈。作者的意思是:给使用者开放_LOADED手动注册,容易引起工程问题。举例:假如lua-protobuf有个丰富的生态,那么当用户使用这个庞大生态中的某一个工具的时候,这个工具应该默认不会集成lua-protobuf,lua-protobuf可能会需要用户手动集成,那么在开放了_LOADED手动注册的情况下,他要是命名成pb3,那么这个生态的工具用了require “pb”的地方变得都要改一遍。讨论问题是为了解决问题,如果某一方憋不住,率先把情绪带入进来,那么往往问题无法得到解决,光剩下争谁对谁错。还望双方心平气和的好好谈,讨论出一个折中方案。

NewbieGameCoder commented 5 years ago

大家都是为了国内开源生态圈越来越好,很多东西可以冷静下来,在认知到彼此的关切之后,在某个qq群或者某种通讯方式加成的情况下,讨论出结果

topameng commented 5 years ago

static const luaL_Reg loadedlibs[] = { {"_G", luaopen_base}, {LUA_LOADLIBNAME, luaopen_package}, {LUA_COLIBNAME, luaopen_coroutine}, {LUA_TABLIBNAME, luaopen_table}, {LUA_IOLIBNAME, luaopen_io}, {LUA_OSLIBNAME, luaopen_os}, {LUA_STRLIBNAME, luaopen_string}, {LUA_MATHLIBNAME, luaopen_math}, {LUA_UTF8LIBNAME, luaopen_utf8}, {LUA_DBLIBNAME, luaopen_debug},

if defined(LUA_COMPAT_BITLIB)

{LUA_BITLIBNAME, luaopen_bit32},

endif

{NULL, NULL} };

LUALIB_API void luaL_openlibs (lua_State L) { const luaL_Reg lib; / "require" functions from 'loadedlibs' and set results to global table / for (lib = loadedlibs; lib->func; lib++) { luaL_requiref(L, lib->name, lib->func, 1); lua_pop(L, 1); / remove lib / } } 你需要的是自己提供一个这样的函数,luac 自身内部都有luaL_openlibs,LUA_XXXXLIBNAME 这些库的名字就不应该外部来设置,即使你坚持自己的看法,也需要调用者去做,我这只是中间bridge层,该有的功能都有。你觉得的不需要自己提供,就不管好了。让调用者去做,就像我上面列的代码,但我不保证那些名字跟你lua脚本中的库名字一致。 你的脚本里如果写了require "pb"。但我注册时候: lua.OpenLibs(LuaDLL.luaopen_pb); luaState.LuaSetField(-2, "pb3"); 这就对不上。让调用者指定固定名字,这是非常不合理的。 当然这也不关中间层什么事,LuaClient 只是一个初始化例子,有些包含在tolua内部库就给了初始话代码,如果其他lua库,需要用户自己参考例子。当然对所有上层来说都只希望一个luaL_openlibs解决问题。没有的话对于lua set table k, v 就三个函数,没必要再多一个luaL_Reg 结构。而且是否写入_preload 只有库作者考虑性能和是否常用问题。调用者也不会做这个考虑

NewbieGameCoder commented 5 years ago

lua本身的 static const luaL_Reg loadedlibs[] = { {"_G", luaopen_base}, {LUA_LOADLIBNAME, luaopen_package}, {LUA_COLIBNAME, luaopen_coroutine}, {LUA_TABLIBNAME, luaopen_table}, {LUA_IOLIBNAME, luaopen_io}, {LUA_OSLIBNAME, luaopen_os}, {LUA_STRLIBNAME, luaopen_string}, {LUA_MATHLIBNAME, luaopen_math}, {LUA_UTF8LIBNAME, luaopen_utf8}, {LUA_DBLIBNAME, luaopen_debug},

if defined(LUA_COMPAT_BITLIB)

{LUA_BITLIBNAME, luaopen_bit32},

endif

{NULL, NULL} };

LUALIB_API void luaL_openlibs (lua_State L) { const luaL_Reg lib; / "require" functions from 'loadedlibs' and set results to global table / for (lib = loadedlibs; lib->func; lib++) { luaL_requiref(L, lib->name, lib->func, 1); lua_pop(L, 1); / remove lib / } }

这种形式应该算是一种不错的方案(也不是唯一方案),可以参考。大家都和气、和气、和气!也不应看支持的人多人少来看待此次讨论,大家理性讨论,不玩站队,不纠结于互相甩锅,理性,理性。

topameng commented 5 years ago

对于5.1.5库,采用5.1.5形式luaL_openlibs 调用函数形式注册,无法注册按照5.2+规则写的库。如果完全采用5.3 使用luaL_requiref注册的函数形式 ,对于5.1.5用luaL_register 注册函数就无法兼容。首先库要支持5.1.5编译。至于5.3等luac5.3 合入在解决了

//对于5.1.5 这样无法注册lua-protobuf LUALIB_API void luaL_openlibs (lua_State L) { const luaL_Reg lib = lualibs; for (; lib->func; lib++) { lua_pushcfunction(L, lib->func); lua_pushstring(L, lib->name); lua_call(L, 1, 0); } } 对于5.1.5 这个跟直接调用函数区别不大,只需要在库加载完清堆栈即可

starwing commented 5 years ago

兼容的方法我都已经在顶楼给你了啊……这是链接: https://github.com/keplerproject/lua-compat-5.3/blob/ba065189ae67822ac9c69cc6ba633e3e8e123ef6/c-api/compat-5.3.c#L548

这个写法是同时兼容Lua 5.1 5.2 5.3的,为啥不用呢?

starwing commented 5 years ago

现在的问题是这样的,我是一个Lua的C模块库的作者,并不是游戏项目的工作者,目前做的也不是客户端相关的工作。而我的这个库的使用者有一部分用的是tolua的库,他们需要自己将我的库集成到自己的项目里去,而现在tolua的处理方式会给他们造成困难。他们的主观的改动方式是直接改我的C代码,而不是在C#层去包装。这一点是我控制不了的。所以我希望能能有一个更简单的注册方式,仅此而已。

@NewbieGameCoder 其实不是开不开放_LOADED的问题,事实上现在就是开放的,我只是希望能够把现在开放的裸的接口给包装一下而已,C的写法上面给出了,只需要翻译成C#即可,下面是C的源代码,直接转过来了:

COMPAT53_API void luaL_requiref (lua_State *L, const char *modname,
                                 lua_CFunction openf, int glb) {
  luaL_checkstack(L, 3, "not enough stack slots available");
  luaL_getsubtable(L, LUA_REGISTRYINDEX, "_LOADED");
  if (lua_getfield(L, -1, modname) == LUA_TNIL) {
    lua_pop(L, 1);
    lua_pushcfunction(L, openf);
    lua_pushstring(L, modname);
    lua_call(L, 1, 1);
    lua_pushvalue(L, -1);
    lua_setfield(L, -3, modname);
  }
  if (glb) {
    lua_pushvalue(L, -1);
    lua_setglobal(L, modname);
  }
  lua_replace(L, -2);
}
NewbieGameCoder commented 5 years ago

我好奇问一下,lua-protobuf是怎么做到在Lua5.1+, LuaJIT 2.0+中开箱即用的?要不要用户处理“_LOADED”?

topameng commented 5 years ago

请参考下其他库支持5.1.5https://github.com/topameng/tolua_runtime/blob/654e0da626328a6c4cf9824c4e50f31109ddae4e/bit.c#L182,如果不直接支持5.1.5环境,调用库方式很容易造成混淆,尚不确定是否能用5.3 requiref 加载函数使用luaL_register 注册库 。tolua代码也有只支持库注册loaded的例子了。调用者也可以实现注册功能。。个人感觉代码加上编译宏多支持一种环境更可取,这也是有些人直接改库代码原因吧。仅仅建议。luaL_requiref 在5.3下直接导出到c#环境比较容易,新的luac53运行库会用到这个函数,5.1下在c#下仿写反倒有些繁琐。或许有时间在runtime 库里实现一个。

不好意思上面这里https://github.com/topameng/tolua/issues/168#issuecomment-527385570有点武断了,没有去尝试,luaL_requiref 对luaL_register 注册可能也是支持的,明天再看了。

starwing commented 5 years ago

我好奇问一下,lua-protobuf是怎么做到在Lua5.1+, LuaJIT 2.0+中开箱即用的?要不要用户处理“_LOADED”?

任何正常写的C库都能支持啊…… 具体做法是这样的。首先,代码用Lua5.2+的风格直接写。然后问题就是Lua 5.1没有luaL_newlib,那就实现一个好了: https://github.com/starwing/lua-protobuf/blob/7767faf3830d6c232f6ceacacc2633ae22d93dec/pb.c#L52

#define luaL_newlib(L,l) (lua_newtable(L), luaL_register(L,NULL,l))

无论是哪个版本的Lua,luaL_register也只是顺便注册进_LOADED的,如果没有luaL_register,用户调用的require的后续动作依然是将luaopen_的返回值放进_LOADED,因此直接什么都不做返回一个表也是合法的。在Lua5.2+,事实上只有这一种写_LOADED的方法,luaL_register已经废弃了。

starwing commented 5 years ago

@topameng 我这里做的设计选择是。我支持 Lua5.1,但是不支持Lua51 favor的require方式(即直接require,不管返回值,模块会变成全局变量)。即使是Lua5.1,也必须写local pb = require "pb"。因此我不想在代码里放luaL_register。

NewbieGameCoder commented 5 years ago

我好奇问一下,lua-protobuf是怎么做到在Lua5.1+, LuaJIT 2.0+中开箱即用的?要不要用户处理“_LOADED”?

任何正常写的C库都能支持啊…… 具体做法是这样的。首先,代码用Lua5.2+的风格直接写。然后问题就是Lua 5.1没有luaL_newlib,那就实现一个好了: https://github.com/starwing/lua-protobuf/blob/7767faf3830d6c232f6ceacacc2633ae22d93dec/pb.c#L52

#define luaL_newlib(L,l) (lua_newtable(L), luaL_register(L,NULL,l))

无论是哪个版本的Lua,luaL_register也只是顺便注册进_LOADED的,如果没有luaL_register,用户调用的require的后续动作依然是将luaopen_的返回值放进_LOADED,因此直接什么都不做返回一个表也是合法的。在Lua5.2+,事实上只有这一种写_LOADED的方法,luaL_register已经废弃了。

我二逼了,没去看宏定义,请忽略

topameng commented 5 years ago

luaL_register 如果第二个参数为NULL是不会进LOADED的,只起到luaL_setfunc作用。一般把自己库名字带上才行。这里指针对5.1.5式注册。但luajit2.1 其实有luaL_newlib这个函数 更常见定义像这样

define luaL_newlib(L,f) luaL_register(L,"lpeg",f)

topameng commented 5 years ago

如果完全从reqire走preload注册进loaded表,可以走如下方式,比opencjson略简单,但跟其他库注册也是不统一的。 luaState.BeginPreLoad(); luaState.RegFunction("socket.core", new LuaCSFunction(LuaOpen_Socket_Core));
luaState.EndPreLoad();

NewbieGameCoder commented 5 years ago

@starwing 个人扯远点,lua5.1或者luajit里面require加载这个库,得用户自己定义loader或者设置cpath之类的哈?直接调用luaopen_pb是不得行的不哈?那是不上作者暴露出个loader定义流程,或者啥preload流程即可了哈

NewbieGameCoder commented 5 years ago

image 初步测试,此种方法可以集成,不用修改lua-protobuf库,也不用改tolua相关库