u/Polanas

▲ 5 r/lua

Storing references to lua types inside LuaJIT FFI types.

TIL: yes, this can be done. And yes, it is completely safe: you cannot cause UB with this.

local ffi = require("ffi")

--Declaring our ffi struct that will hold the data.
ffi.cdef([[
  typedef struct {
    void* data_ref;
  } LuaTable;
]])

--Can be any lua type stored on heap (table, function, thread, userdata).
local data = function()
  print("hello world")
end

--We get the actual address and store it in a lua number.
local data_ptr_id = tonumber(tostring(data):match("0x%x+"))

--The external storage. That's the only way to get the object back from C land.
local storage = {}
storage[data_ptr_id] = data

--(You can set __mode for this table so that it stores values weakly. But then all ffi structs wouldn't be able to "own" the data inside them.
--To actually own the data and correctly let go of it, you'd need to make a reference counter primitive, like a shared_ptr<T> in C++ or Rc<T> in Rust.
--This counter will also need to be an ffi struct so that we can set a ffi.gc() callback for it.
--The callback would do `storage[data_ptr_id] = nil`, but *only* when there are no longer any references to it. 
--So it basically lets go of the data and to let GC manage it. Remember that regular tables could also store our object!
--You'll need to make sure that each reference counter primitive (lets just call it Rc) holds a unique object, or, in other words, there can't be two Rcs holding the same object. 
--Otherwise, after any of them get freed, the object will get prematurely released.)

--Here's our ffi struct.
local table_ffi = ffi.new("LuaTable", ffi.cast("void*", data_ptr_id))

--And here's how we get it back.
local also_data = storage[tonumber(ffi.cast("uintptr_t", table_ffi.data_ref))]

--If the object's been collected by the GC already, we'll just get a nil, so no use after free opportunities.

--Enjoy!
also_data()
reddit.com
u/Polanas — 7 days ago