IT技术博客大学习 共学习 共进步

使用 luajit 的 ffi 绑定 zeromq

云风的 BLOG 2011-08-22 12:23:19 浏览 4,163 次

    最近 Lua 社区非常活跃。6 月 22 日发布了 Lua 5.2.0 (beta-rc2) 。今天(6 月 24 日) 发布了 LuaJIT-2.0.0-beta8 。

    虽然 luajit 和 lua 5.2 还有点小矛盾,luajit 没有完全支持 lua 5.2 的迹象。不过,这些对 Lua 社区都是好消息啦。可能对于 lua 用户会有点小纠结,到底是追随官方的 5.2 版呢,还是去用性能更好的 luajit2 。我比较在意性能,暂时先投靠 luajit 了。反正和 5.2 区别也不大。更重要的是,luajit2 提供的 ffi 库相当之好用,极大的减少了我们写 C 库的 lua binding 的负担。从某种角度可以看到另一个问题,为基础设施模块设计出良好的 C 接口(而不是 C++ 的)是多么的重要。

    zeromq 是用 C++ 实现的,但它提供的是简洁纯粹的 C 接口。这让它相当利于 binding 到其它语言中使用。之前,已经有了成熟的 lua-zmq 可供使用。它分别实现了 ffi 和不带 ffi 的版本。不过也正因为此,封装层包裹的很淡疼。如果只支持 ffi 版本的话,其实这个工作可以做的非常简洁。

    出于实践 luajit ffi 库的目的,也为了让这部分代码看起来清爽一点。我花了半个下午自己封装了一下 zeromq 。所用时间比在 windows 下配置安装那个现成的 lua-zmq 所用时间看起来更少(不需要装 msys ,cmake 等等淡疼的玩意)。谁再下面留言说不要重复造轮子了,我也不打算跟它急了。吵架的时间都比写代码时间长。我们从来不会把写一遍 hello world 看成重新制造轮子不是么?使用 ffi 去 binding C 库实在是太容易了,不比写 hello world 更复杂。

    为了鼓励各位同学自己造轮子,我就不把我的实现完整列出来了。希望在 lua 下使用 zeromq 却懒的学习的同学请自己下载上面提到 binding 库好了。

    说说我的体会:

    zeromq 在 windows 下用 gcc 编译挺麻烦的,我是找一个装了 vc 的同学帮我利用官方的 vs 工程编译出的 dll 文件使用。因为都是 C 接口,所以可以直接 link 到 gcc 项目里。

    zeromq 的 api 设计的很精巧合理。总数量扳着手指都数的过来。不过我们对其做适当改造会做起来更轻松一点。

    比如:

    void *zmq_init (int io_threads);

    它返回的是一个 context 对象,出于 C API 设计方便,所以使用的是 void * 。

    后面的

    void *zmq_socket (void *context, int type);

    接受参数正是这个 context 对象,并返回一个 socket 对象,这里依然是 void * 。

    我做了一点小修改,把这两个 api 改为了:

    typedef struct {} context;

    context *zmq_init (int io_threads);

    typedef struct {} socket;

    socket *zmq_socket (context *context, int type);

    就是说,给它们起了专门的类型名。这样做有什么好处呢?在 lua 代码中,我们可以利用 ffi.metatype 给它们加方法了。虽然 lua 本身的 table 也可以有 metatable 。但利用 ffi.metatype 可以少一个间接层,性能更好,代码更简洁。

    我是这样写的:

local context = {}

function init(io_threads)
    return newobj(libzmq.zmq_init(io_threads or 1))
end

function context:term()
    return check(libzmq.zmq_term(self))
end

-- Socket types
PAIR = 0
PUB = 1
SUB = 2
REQ = 3
REP = 4
DEALER = 5
ROUTER = 6
PULL = 7
PUSH = 8
XPUB = 9
XSUB = 10

local socket = {}

function context:socket(type)
    return newobj(libzmq.zmq_socket(self, type))
end

ffi.metatype("context", { __index = context } )

    这里 newobj 函数检查返回值是不是 NULL ,如果返回空指针,就利用 strerror 取得错误信息。

local function strerror()
    return ffi.string(libzmq.zmq_strerror(libzmq.zmq_errno()))
end

local function newobj(obj)
    if ffi.cast("void *",obj) > nil then
        return obj
    else
        return false , strerror()
    end
end

    我在 ffi 文档中没有找到明确的,判断一个指针是不是 NULL 的方法,琢磨了一下,应该是用 ffi.cast("void *",obj) > nil 不知道是不是惯例。

    msg 对象我做的时候保留了完全一致的 C api ,但实际在高层使用的时候不太方便,所以做了进一步的 lua 层封装。类似这样的:

function socket:send_string(str)
    local m = msg(str)
    local err = self:send(m)
    m:close()
    return err
end

function socket:recv_string()
    local m = msg()
    local err = self:recv(m)
    if err == nil then
        local ret = m:tostring()
        m:close()
        return ret
    else
        return false, err       
    end
end

    不过我认为性能会有点小问题,如果用 lua 做比较底层的设施,我认为还是用更直接的 msg api 比较好。我想,zeromq 的应用场合偏中低层。应该能够定义出明确需求。使用 luajit 开发,我们在这个层次上应多从 C 的角度去思考(良好使用的 luajit 可以达到 C 的性能级别,但需要多理解和推敲,jit 也不是免费的午餐)。做出更好的封装后,再提供更高层次的接口供上层调用。不大有必要在下面的层次考虑便利性。

    setsockopt 和 getsockopt 从 C 接口的角度上看,提供了足够多的灵活性。但表现为 lua 的 api 我认为不宜照搬。因为 C 接口不宜扩展,但 lua 却不一样。这个最好还是根据需要,封装更具体的功能。这些也不是性能敏感的接口,可以多做一些类型检查。


    最后正经点说,没有全部公开源代码,其实是因为我觉得根据我的个人需求,我希望讲 zeromq 和 google protobuf 做更紧密的结合。放在一起做成一个完整的功能模块。我之前为 lua 定制了一个 protobuf 库 同样也没有开源,也是因为我认为其功能不完整。

    所以更进一步的工作是把它们做深度的整合。

建议继续学习

  1. Nginx与Lua (阅读 5,565)
  2. Lua GC 的源码剖析 (2) (阅读 4,943)
  3. Ameba , 一个简单的 lua 多线程实现 (阅读 4,844)
  4. Lua GC 的源码剖析 (4) (阅读 4,604)
  5. Proto Buffers in Lua (阅读 4,283)
  6. Lua GC 的源码剖析 (1) (阅读 4,228)
  7. ZeroMQ 的模式 (阅读 3,960)
  8. 一个 Lua 内存泄露检查工具 (阅读 3,885)
  9. Lua GC 的源码剖析 (6) 完结 (阅读 3,782)
  10. ZeroMQ的学习和研究 (阅读 3,785)