▲ 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()
u/Polanas — 7 days ago