starwing / lua-protobuf

A Lua module to work with Google protobuf
MIT License
1.73k stars 387 forks source link

是否考虑拆分C 接口和Lua接口 #203

Open changnet opened 2 years ago

changnet commented 2 years ago

当前这个库是只提供了Lua的接口,并且相关的数据结构(如lpb_State)是放在pb.c,在不修改原代码的情况下几乎无法对这个库进行二次开发

例如,通过socket的协议,不能直接在c调用decode函数来解析,哪怕想重新实现一个decode接口也不行,因为include不到对应的数据结构。又或者想实现不使用lua table而是lua栈来encode、decode,也无法在不修改原代码的情况下实现。

我知道大概是不会拆分的,现有的设置和Lua耦合比较紧密,拆分是需要大改。只是问一下对这事的看法

starwing commented 2 years ago

的确有这个问题,所以在pb.c里提供了unsafe接口用于和其他C库交互。但是对于导出的话,为了性能目前的确耦合的很紧密,不太方便提供对应的数据类型。但是我可以考虑提供一个lpb.h用于提供一些不透明类型和API接口。可以先提出一下需要哪些API吗?这样可以规划下如何提供这些接口。

changnet @.***>于2022年6月11日 周六18:01写道:

当前这个库是只提供了Lua的接口,并且相关的数据结构(如lpb_State)是放在pb.c,在不修改原代码的情况下几乎无法对这个库进行二次开发

例如,通过socket的协议,不能直接在c调用decode函数来解析,哪怕想重新实现一个decode接口也不行,因为include不到对应的数据结构。又或者想实现不使用lua table而是lua栈来encode、decode,也法在不修改原代码的情况下实现。

我知道大概是不会拆分的,现有的设置和Lua耦合比较紧密,拆分是需要大改。只是问一下对这事的看法

— Reply to this email directly, view it on GitHub https://github.com/starwing/lua-protobuf/issues/203, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA36M7LBYS7SKALE2DCNYDVORPX5ANCNFSM5YP22H3A . You are receiving this because you are subscribed to this thread.Message ID: @.***>

-- regards, Xavier Wang.

Gowa2017 commented 2 years ago

如果需要在LUA层做更多的话其实你可以考虑一下云风的PBC库

changnet commented 2 years ago

我之前一直是用的pbc,增加部分proto3支持并且使用自己的binding,但是问题是pbc虽然提供了编码、解码相关的C接口,但不具备反射功能。即pbc加载了pb文件后,没有接口获取某个message里有哪些字段,是啥类型。现在pbc基本没在维护,即使自己pr新功能也未必会合并。

目前大概的想法是

  1. 可以在c直接调用的encode、decode接口,以适应不同的框架,并不是所有的框架接口都是从lua调用的,例如
    
    /**
    * @brief 把lua table打包到缓冲区中
    * @param LS lpb_State指针
    * @param L lua虚拟机
    * @param index 需要打包的lua table在栈中的索引
    * @param buffer 用于存放编码后数据的缓冲区指针
    * @param buffer_len 缓存区长度,编码后的数据大于该长度将失败
    * @return 成功返回0
    */
    int encode(lpb_State *LS, lua_State* L, int index, char *buffer, size_t buffer_len);

/**

  1. encode数据时,可以从栈取数据,而不是通过lua table。同样的,decode数据时,不是生成lua table,而是平铺到栈上。例如
    
    local id = 18
    local name = "ilse"
    local email = "888888888@github.com"
    local phone = {
         { type = 0, number = "12312341234" },
         { type = 2,   number = "45645674567" }
      }

-- 传入的参数,必须严格按proto文件中指定的顺序 pb.pack("tutorial.Person", name, id, email, phone)

name, id, email, phone = pb.unpack("tutorial.Person", buffer)



对于第一个问题,考虑把pb.c中的一些数据结构和接口声明放到一个头文件中(如`pb_internal.h`),有了数据结构和接口,就可以自己实现自己的encode、decode。这个不需要修改逻辑,只需要调整一下代码结构

但这引发的一个问题是,在c调用接口,是不应该使用`luaL_error luaL_checkstack`等函数来处理异常的,因此异常处理需要在`lpb_Env`加一个回调函数指针来处理

对于第二个问题,主要问题是encode时数据来源不是table,需要另外实现第一层lpb_encode,从栈取数据而不是table。另外decode时,第一层message的数据不是设置到lua table,而是放到栈上即可,同样需要另外实现第一层lpbD_message

这只是一个初步的想法

另外,我并不是要在Lua层做更多,而是要在c层做更多。
starwing commented 2 years ago

对于第一个需求,可以考虑提供接口,但是pb终归是给Lua用的,你最终给socket也是提供给Lua具体的表的,我看不出来将读取循环放在Lua里会有什么问题。在pb.c里有创建Slice的方法,你可以用lpb_newslice得到一个slice,并且设置它的值,然后在Lua里传递给pb:

socket.receive(slice)
local pkg = pb.decode("xxx", slice)

也可以pb.encode到一个pb.Buffer,然后在C里处理。我没看出来有什么不方便的地方。你有什么必须的地方一定需要一个C接口的理由吗?

对于第二个,这个几乎完全是一个全新的需求,它涉及到很多麻烦的地方(比如栈上最多255个,比如如何表达嵌套数据结构)。注意到Lua的原则table是Lua唯一的数据结构,你的这个需求必然是小众和有限制的。如果你有这样的需求,你可以使用底层接口(buffer, slice等),完全可以用纯Lua做到这个效果,但是性能问题你自己承担。如果要用C来实现,这个就属于为了2%的需求要写98%的代码的示例了。你可以考虑fork本项目然后自己改自己用甚至开源,但是这不是本项目支持的范畴。当然同样的,如果你有非常完善的理由说明有特殊情况必须要这种接口不可(而不是为了所谓方便或者效率?)那么我会缓慢地考虑做支持(比如按顺序encode这个需求就是提了好几年我才想到办法用简单的方式做到了支持),但是时间上就不会很快了。

Gowa2017 commented 2 years ago

我之前一直是用的pbc,增加部分proto3支持并且使用自己的binding,但是问题是pbc虽然提供了编码、解码相关的C接口,但不具备反射功能。即pbc加载了pb文件后,没有接口获取某个message里有哪些字段,是啥类型。现在pbc基本没在维护,即使自己pr新功能也未必会合并。

目前大概的想法是

  1. 可以在c直接调用的encode、decode接口,以适应不同的框架,并不是所有的框架接口都是从lua调用的,例如
/**
 * @brief 把lua table打包到缓冲区中
 * @param LS lpb_State指针
 * @param L lua虚拟机
 * @param index 需要打包的lua table在栈中的索引
 * @param buffer 用于存放编码后数据的缓冲区指针
 * @param buffer_len 缓存区长度,编码后的数据大于该长度将失败
 * @return 成功返回0
*/
int encode(lpb_State *LS, lua_State* L, int index, char *buffer, size_t buffer_len);

/**
 * @brief 解析protobuf数据到lua table
 * @param LS lpb_State指针
 * @param L lua虚拟机
 * @param buffer 待解析protobuf数据
 * @param buffer_len buffer的长度
 * @return 成功返回0
*/
int decode(lpb_State* LS, lua_State* L, char *buffer, size_t buffer_len);
  1. encode数据时,可以从栈取数据,而不是通过lua table。同样的,decode数据时,不是生成lua table,而是平铺到栈上。例如
local id = 18
local name = "ilse"
local email = "888888888@github.com"
local phone = {
         { type = 0, number = "12312341234" },
         { type = 2,   number = "45645674567" }
      }

-- 传入的参数,必须严格按proto文件中指定的顺序
pb.pack("tutorial.Person", name, id, email, phone)

name, id, email, phone = pb.unpack("tutorial.Person", buffer)

对于第一个问题,考虑把pb.c中的一些数据结构和接口声明放到一个头文件中(如pb_internal.h),有了数据结构和接口,就可以自己实现自己的encode、decode。这个不需要修改逻辑,只需要调整一下代码结构

但这引发的一个问题是,在c调用接口,是不应该使用luaL_error luaL_checkstack等函数来处理异常的,因此异常处理需要在lpb_Env加一个回调函数指针来处理

对于第二个问题,主要问题是encode时数据来源不是table,需要另外实现第一层lpb_encode,从栈取数据而不是table。另外decode时,第一层message的数据不是设置到lua table,而是放到栈上即可,同样需要另外实现第一层lpbD_message

这只是一个初步的想法

另外,我并不是要在Lua层做更多,而是要在c层做更多。

我曾经以为没有 其实有的你看要 demo test。 就知道了了

changnet commented 2 years ago

对于第一个需求,可以考虑提供接口,但是pb终归是给Lua用的,你最终给socket也是提供给Lua具体的表的,我看不出来将读取循环放在Lua里会有什么问题。在pb.c里有创建Slice的方法,你可以用lpb_newslice得到一个slice,并且设置它的值,然后在Lua里传递给pb:

socket.receive(slice)
local pkg = pb.decode("xxx", slice)

也可以pb.encode到一个pb.Buffer,然后在C里处理。我没看出来有什么不方便的地方。你有什么必须的地方一定需要一个C接口的理由吗?

对于第二个,这个几乎完全是一个全新的需求,它涉及到很多麻烦的地方(比如栈上最多255个,比如如何表达嵌套数据结构)。注意到Lua的原则table是Lua唯一的数据结构,你的这个需求必然是小众和有限制的。如果你有这样的需求,你可以使用底层接口(buffer, slice等),完全可以用纯Lua做到这个效果,但是性能问题你自己承担。如果要用C来实现,这个就属于为了2%的需求要写98%的代码的示例了。你可以考虑fork本项目然后自己改自己用甚至开源,但是这不是本项目支持的范畴。当然同样的,如果你有非常完善的理由说明有特殊情况必须要这种接口不可(而不是为了所谓方便或者效率?)那么我会缓慢地考虑做支持(比如按顺序encode这个需求就是提了好几年我才想到办法用简单的方式做到了支持),但是时间上就不会很快了。

从需求上来讲,并不是必须要一个C接口的,就纯粹是之前的代码结构是在C处理这一块逻辑,如果可以,那就没有必要牺牲少量性能做一次lua回调。我也并没有期望在这个库地提供一套C接口,没必要的,而是期望在有相关头文件的情况下,能自己实现。我初步做了测试,假如提供了头文件和错误回调函数,那只需要在自己的代码里使用几行代码就能实现这两个函数:

int Lext_encode(void* LS, lua_State* L, int index, pb_Buffer *b)
{
    // Lpb_encode(L);
    lpb_Env e;
    const pb_Type* t = lpb_type(LS, lpb_checkslice(L, index));

    luaL_checktype(L, index + 1, LUA_TTABLE);

    // lpb_encode要求栈顶必定得是要打包的数据
    if (lua_gettop(L) != index + 1) return -1;

    argcheck(L, t != NULL, 1, "type '%s' does not exists", lua_tostring(L, 1));

    e.L = L, e.LS = LS, e.b = b;
    // 打包的数据,是写到了pb_Buffer *b = e->b;

    lpb_encode(&e, t);

    return 0;
}

int lext_decode(void* LS, lua_State* L, pb_Slice* s)
{
    // pb_lslice(data, size) 从const char *构建slice
    const pb_Type* t = lpb_type(LS, lpb_checkslice(L, 1));
    lpb_Env e;
    argcheck(L, t != NULL, 1, "type '%s' does not exists", lua_tostring(L, 1));

    lpb_pushtypetable(L, LS, t);
    e.L = L, e.LS = LS, e.s = s;
    return lpbD_message(&e, t);
}

对于第二个问题,它其实并没有想象中复杂。注意示例中只有第一层message是平铺到栈上的,嵌套的message,如上面例子中的phone这个结构还是一个lua table,所以需要修改的其实并不多,仅是第一层encode、decode的逻辑。提供这个接口的可以减少大量table的创建,写代码时也不需要繁琐地构建一个table。在游戏中协议的收发是一个非常基础而且频繁使用,但很少去修改的功能,我觉得提供这个接口还是有意义的

具体的实现,我会先fork一份代码来实现,到时候再看看改动有多大。因为这个仅是我探讨的一些优化,所以也是需要花一段时间

starwing commented 2 years ago

@changnet 既然你打算自己先实现一下试试了,那么就祝愿你能顺利实现啦,为了方便你实现,这里提几个点:

  1. 不要导出任何结构,不然可能导致版本不同而类型结构不匹配,使用不透明类型做这个事。
  2. 推荐你还是看看lpb_newslice这个函数,推荐用相同的方法做这件事:

    1. 直接在pb.c里提供一些基本的函数的实现(可以把现在某些函数的static改成LUALIB_API的)
    2. 单独提供一个lpb.h文件,内容可以是:

      #include <lua.h>
      #include <lauxlib.h>
      #include <lualib.h>
      
      LUALIB_API int lpb_newslice(lua_State *L, const char *s, size_t len);

      这样,可以做到这个头文件和pb.c的完全解耦。

然后,要做的事情,就是选择暴露的函数,并且写一些可能的包装函数了~

starwing commented 2 years ago

另外关于错误处理函数:Lua接口直接报错其实是很正常的,如果要处理错误,那么应该用户那边来做lua_pcall

starwing commented 2 years ago

我做了一个 https://github.com/starwing/lua-protobuf/tree/capi 分支写了些初步的C API代码,你看看是不是满足你的需求。

changnet commented 2 years ago

先初步实现了根据message pack unpack的接口,如果觉得可以,可以先合并这两个接口

c api的话,我再考虑下方案。主要是现有的错误处理走的是longjump,而不是返回值。这对 socket收到数据 --> c api --> lua 这个流程的调用不是太友好。因为在c api之前,是没有lua_pcall的,longjump就直接跳到上一次lua_pcall的地方了