Files
fs2open.github.com/code/scripting/lua/LuaThread.cpp
T
Goober5000andClaude Fable 5 9bf5b6acf4 harden move operations in w_bank and LuaThread
w_bank: add a move constructor.  object_copy_including_array_member
returns by value, and without a move constructor the return fell back
to the shallow copy constructor whenever NRVO was not applied (e.g.
MSVC debug builds), after which the local's destructor deleted the
freshly allocated arrays -- leaving the stored bank dangling and
double-deleting at model unload.  Also add noexcept to the move
operations, replace the destructor call in move-assignment with
explicit delete[]s (using an object after its lifetime has ended is
undefined behavior), guard against self-move, and document that the
defaulted copy constructor is intentionally shallow for
object_copy_including_array_member's copy-then-replace pattern.

LuaThread: declare the defaulted move operations noexcept.  This
exposed that LuaTable was not movable at all (its user-declared
destructor suppressed the implicit moves), so LuaThread's implicit
move spec was formally throwing -- a hard error on gcc 9, which
still requires the spec on a defaulted redeclaration to match the
implicit one.  Restore LuaTable's rule of five and declare the
LuaValue and LuaFunction copy operations noexcept (they only copy a
shared_ptr, a raw pointer, and an enum), making the noexcept claims
hold throughout the hierarchy.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-06 13:33:49 -04:00

163 lines
5.7 KiB
C++

#include "LuaThread.h"
#include "LuaException.h"
#include <utility>
namespace luacpp {
LuaThread LuaThread::create(lua_State* L, const LuaFunction& func)
{
auto luaThread = lua_newthread(L);
LuaThread thread(L, luaThread);
thread.setReference(UniqueLuaReference::create(L));
lua_pop(L, 1);
//Make sure that the C++-side reference of the LuaThread (and everything it holds) is cleared when its parents references are auto-garbage collected by lua for whatever reason (usually due to the parent thread dying).
//To do this, register an object with a __gc method in the thread (usually we'd want to directly attach it to the thread, but only userdata will have __gc methods called).
//Usually we'd want to do this when the childs references are GC'd, but creating tables on child threads from C causes tests to fail for some reason.
auto threadRef = std::weak_ptr<UniqueLuaReference>(thread.getReference());
//These must be pointers to weak pointers, as we cannot know the weak pointers before creating the lambda.
auto delFuncRef = std::make_shared<std::weak_ptr<UniqueLuaReference>>();
auto delTabRef = std::make_shared<std::weak_ptr<UniqueLuaReference>>();
auto delUserdataRef = std::make_shared<std::weak_ptr<UniqueLuaReference>>();
int stack = lua_gettop(L);
//Make sure to create the function that clears the LuaThread reference BEFORE creating the userdata value, otherwise the function will be garbage-collected itself before it's called.
thread.deleterFunc = LuaFunction::createFromStdFunction(L, [threadRef, delFuncRef, delTabRef, delUserdataRef](lua_State*, const LuaValueList&) -> LuaValueList {
if (!threadRef.expired())
threadRef.lock()->removeReference();
if (!delFuncRef->expired())
delFuncRef->lock()->removeReference();
if (!delTabRef->expired())
delTabRef->lock()->removeReference();
if (!delUserdataRef->expired())
delUserdataRef->lock()->removeReference();
return {};
});
//NOW, create the dummy userdata, and its metatable
lua_newuserdata(L, sizeof(bool));
thread.deleterUserdata = UniqueLuaReference::create(L);
thread.deleterTable = LuaTable::create(L);
*delFuncRef = std::weak_ptr<UniqueLuaReference>(thread.deleterFunc.getReference());
*delTabRef = std::weak_ptr<UniqueLuaReference>(thread.deleterTable.getReference());
*delUserdataRef = std::weak_ptr<UniqueLuaReference>(thread.deleterUserdata);
//Since we hold references to all we need, tidy up the stack.
lua_settop(L, stack);
//Push the values in the correct order for assembly. Userdata, then table, then func
thread.deleterUserdata->pushValue(L);
thread.deleterTable.pushValue(L);
thread.deleterFunc.pushValue(L);
lua_setfield(L, -2, "__gc"); //Takes the top value (func) and assigns it to the second to last one (table) as __gc, and pops the top value
lua_setmetatable(L, -2); //Takes the top value (table) and assigns it as the metadata table of the second to last one (userdata), and pops the last value
lua_pop(L, 1); //Pop the userdata
func.pushValue(L);
// Move the main function to the thread (I have no idea what this actually does but the Lua code does the same...)
lua_xmove(L, luaThread, 1);
return thread;
}
LuaThread::LuaThread() = default;
LuaThread::LuaThread(lua_State* luaState, lua_State* thread) : LuaValue(luaState), _thread(thread) {}
LuaThread::LuaThread(LuaThread&&) noexcept = default;
LuaThread& LuaThread::operator=(LuaThread&&) noexcept = default;
LuaThread::~LuaThread() = default;
void LuaThread::setReference(const LuaReference& ref)
{
lua_State* L = ref->getState();
ref->pushValue(L);
if (lua_type(L, -1) != LUA_TTHREAD) {
lua_pop(L, 1);
throw LuaException("Reference does not refer to a thread!");
} else {
lua_pop(L, 1);
LuaValue::setReference(ref);
}
}
LuaThread::ResumeState LuaThread::resume(const LuaValueList& params) const
{
int nargs = static_cast<int>(params.size());
for (const auto& val : params) {
val.pushValue(_luaState);
}
// Move parameters to the thread
lua_xmove(_luaState, _thread, nargs);
// now resume
const auto result = lua_resume(_thread, nargs);
if (result != 0 && result != LUA_YIELD) {
if (_errorCallback) {
if (_errorCallback(_luaState, _thread)) {
// If the error was handled just pretend that we resumed successfully
ResumeState resume;
resume.completed = true;
return resume;
}
}
throw LuaException("Lua coroutine failed to resume!");
}
LuaValueList returnVals;
auto numRet = lua_gettop(_thread);
returnVals.reserve(numRet);
// Move the values back to the main state so that it uses the right state
auto previousStack = lua_gettop(_luaState); // Keep this for cleaning up later
lua_xmove(_thread, _luaState, numRet);
auto retValsStart = previousStack + 1; // We need to start at + 1 since that is where the first return val would be
for (int i = retValsStart; i <= previousStack + numRet; ++i) {
LuaValue val;
convert::popValue(_luaState, val, i, false);
returnVals.push_back(std::move(val));
}
// Cleanup the stack
lua_settop(_luaState, previousStack);
ResumeState state;
state.completed = result != LUA_YIELD;
state.returnVals = std::move(returnVals);
return state;
}
void LuaThread::setErrorCallback(LuaThread::ErrorCallback errorCallback) { _errorCallback = std::move(errorCallback); }
lua_State* LuaThread::getThreadHandle() const { return _thread; }
bool convert::popValue(lua_State* luaState, LuaThread& target, int stackposition, bool remove)
{
if (!internal::isValidIndex(luaState, stackposition)) {
return false;
}
if (!lua_isthread(luaState, stackposition)) {
return false;
} else {
target.setReference(UniqueLuaReference::create(luaState, stackposition));
if (remove) {
lua_remove(luaState, stackposition);
}
return true;
}
}
} // namespace luacpp