Search code examples
luaffiluajit

How do I convert a cdata structure into a lua string?


I'm in the middle of writing a small application that needs to read some complex binary messages in LuaJit.

I've been using the bit module and string.rep a lot. However, it's all very cumbersome. I'm new to using LuaJit and think there might be a much easier way using FFI.

In C I can declare a structure like this:

struct mystruct
{
  uint32_t field1;
  char     field2[6];
  uin64_t  field3;
  short    field4;
} __attribute__(packed);

In reading LuaJit's FFI it seems you can declare

ffi.cdef[[
    #pragma pack(1)
    struct mystruct
    {
      uint32_t field1;
      char     field2[6];
      uin64_t  field3;
      short    field4;
    };
]]

I can then create a mystruct and access the fields like this:

local ms = ffi.new("mystruct")
ms.field1 = 32;
// ... etc

But, how do I convert this back into a lua string?

I tried this, but it didn't seem to do what I wanted.

local s = tostring(ms)

and this:

local s = ffi.string(ms)

produces the following error "bad argument #1 to 'string' (cannot convert 'struct mystruct' to 'const char *')"

So I tried a cast:

local s = ffi.string(ffi.cast("char*", ms))

No error, but it looks wrong on the wire.


Solution

  • You have to explicitly specify the length when using ffi.string with a non-string-like parameter:

    str = ffi.string(ptr [,len])

    Creates an interned Lua string from the data pointed to by ptr.

    If the optional argument len is missing, ptr is converted to a "char *" and the data is assumed to be zero-terminated. The length of the string is computed with strlen().

    When running the following code, I get the expected (little endian) result:

    ffi = require 'ffi'
    ffi.cdef[[
        typedef unsigned long uint32_t;
        typedef unsigned long long uint64_t;
        #pragma pack(1)
        struct mystruct
        {
          uint32_t field1;
          char     field2[6];
          uint64_t  field3;
          short    field4;
        };
    ]]
    
    function string.tohex(str)
        return (str:gsub('.', function (c)
            return string.format('%02X', string.byte(c))
        end))
    end
    
    ms = ffi.new('struct mystruct', 1, {2, 3, 4, 5, 6, 7}, 8, 9)
    s = ffi.string(ms, ffi.sizeof(ms)) -- specify how long the byte sequence is
    print(s:tohex()) --> 0100000002030405060708000000000000000900
    

    Update: I know this is not a part of the original question, but I just learned this trick, and in order to be complete, here is a way to convert Lua string back to FFI cdata:

        data = ffi.new('struct mystruct')   -- create a new cdata
        ffi.copy(data, s, ffi.sizeof(data)) -- fill it with data from Lua string 's'
        print(data.field1, data.field4)     --> 1   9