diff --git a/.gitignore b/.gitignore index ec4c7da8..e3de883f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,9 @@ Build/ *.vcxproj.filters *.vcxproj.user .vs/ -.vscode -.DS_store +.vscode/ .idea/ +.gitnexus/ +.DS_Store cmake-build-*/ tmpunwind.o diff --git a/Benchmarks/benchmark_luabridge3.cpp b/Benchmarks/benchmark_luabridge3.cpp index b8f855da..10b158b7 100644 --- a/Benchmarks/benchmark_luabridge3.cpp +++ b/Benchmarks/benchmark_luabridge3.cpp @@ -397,14 +397,8 @@ void optional_success_measure(benchmark::State& state) for (auto _ : state) { (void) _; - luabridge::LuaRef warble = luabridge::getGlobal(L, "warble"); - if (warble.isTable()) - { - auto result = warble.getField("value"); - x += result ? *result : 1.0; - } - else - x += 1.0; + auto result = luabridge::tryGetGlobalField(L, "warble", "value"); + x += result ? *result : 1.0; } benchmark::DoNotOptimize(x); @@ -419,14 +413,8 @@ void optional_half_failure_measure(benchmark::State& state) for (auto _ : state) { (void) _; - luabridge::LuaRef warble = luabridge::getGlobal(L, "warble"); - if (warble.isTable()) - { - auto result = warble.getField("value"); - x += result ? *result : 1.0; - } - else - x += 1.0; + auto result = luabridge::tryGetGlobalField(L, "warble", "value"); + x += result ? *result : 1.0; } benchmark::DoNotOptimize(x); @@ -440,14 +428,8 @@ void optional_failure_measure(benchmark::State& state) for (auto _ : state) { (void) _; - luabridge::LuaRef warble = luabridge::getGlobal(L, "warble"); - if (warble.isTable()) - { - auto result = warble.getField("value"); - x += result ? *result : 1.0; - } - else - x += 1.0; + auto result = luabridge::tryGetGlobalField(L, "warble", "value"); + x += result ? *result : 1.0; } benchmark::DoNotOptimize(x); diff --git a/Distribution/LuaBridge/LuaBridge.h b/Distribution/LuaBridge/LuaBridge.h index 0b75d4e5..0ec3ff58 100644 --- a/Distribution/LuaBridge/LuaBridge.h +++ b/Distribution/LuaBridge/LuaBridge.h @@ -740,9 +740,7 @@ auto bind_back(F&& f, BoundArgs&&... args) static constexpr std::size_t num_remaining = num_explicit - num_bound; using explicit_remaining = detail::tuple_take_first_t; - using leading = detail::bind_back_leading_t; - using R = typename FnTraits::result_type; return detail::bind_back_wrapper...>( @@ -4031,6 +4029,12 @@ class Userdata return static_cast(rawPtr); } + template + static T* getExactPointer(lua_State* L, int index) noexcept + { + return static_cast(static_cast(lua_touserdata(L, index))->getPointer()); + } + template static bool isInstance(lua_State* L, int index) { @@ -6083,16 +6087,15 @@ struct Stack> StackRestore stackRestore(L); lua_createtable(L, static_cast(Size), 0); + const int tableIndex = lua_gettop(L); for (std::size_t i = 0; i < Size; ++i) { - lua_pushinteger(L, static_cast(i + 1)); - auto result = Stack::push(L, array[i]); if (! result) return result; - lua_settable(L, -3); + lua_rawseti(L, tableIndex, i + 1); } stackRestore.reset(); @@ -8016,6 +8019,31 @@ struct function } }; +template +TypeResult get_member_object(lua_State* L, bool canBeConst) +{ + const int selfIndex = lua_absindex(L, 1); + + if (lua_getmetatable(L, selfIndex)) + { + if (lua_type(L, lua_upvalueindex(2)) == LUA_TTABLE && lua_rawequal(L, -1, lua_upvalueindex(2))) + { + lua_pop(L, 1); + return Userdata::getExactPointer(L, selfIndex); + } + + if (canBeConst && lua_type(L, lua_upvalueindex(3)) == LUA_TTABLE && lua_rawequal(L, -1, lua_upvalueindex(3))) + { + lua_pop(L, 1); + return Userdata::getExactPointer(L, selfIndex); + } + + lua_pop(L, 1); + } + + return Userdata::get(L, selfIndex, canBeConst); +} + template int invoke_member_function(lua_State* L) { @@ -8023,7 +8051,7 @@ int invoke_member_function(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - auto ptr = Userdata::get(L, 1, false); + auto ptr = get_member_object(L, false); if (! ptr) raise_lua_error(L, "%s", ptr.error_cstr()); @@ -8040,7 +8068,7 @@ int invoke_const_member_function(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - auto ptr = Userdata::get(L, 1, true); + auto ptr = get_member_object(L, true); if (! ptr) raise_lua_error(L, "%s", ptr.error_cstr()); @@ -8444,7 +8472,8 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...), const c using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); } template @@ -8455,7 +8484,8 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...) noexcept using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); } template @@ -8466,7 +8496,9 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...) const, c using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getConstRegistryKey()); + lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 3); } template @@ -8477,7 +8509,9 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...) const no using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getConstRegistryKey()); + lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 3); } template @@ -9531,6 +9565,24 @@ TypeResult getGlobal(lua_State* L, const char* name) return result; } +template +std::optional tryGetGlobalField(lua_State* L, const char* globalName, const char* fieldName) +{ + const StackRestore stackRestore(L); + + lua_getglobal(L, globalName); + if (! lua_istable(L, -1)) + return std::nullopt; + + lua_getfield(L, -1, fieldName); + + auto result = Stack>::get(L, -1); + if (! result) + return std::nullopt; + + return *result; +} + template bool setGlobal(lua_State* L, T&& t, const char* name) { @@ -9705,7 +9757,7 @@ class LuaRefBase return metatable.isTable() && metatable["__call"].isFunction(); } - std::optional getClassName() + std::optional getClassName() const { if (! isUserdata()) return std::nullopt; @@ -9866,7 +9918,7 @@ class LuaRefBase } template - bool rawequal(const T& v) const + [[nodiscard]] bool rawequal(const T& v) const { const StackRestore stackRestore(m_L); @@ -9878,7 +9930,7 @@ class LuaRefBase return lua_rawequal(m_L, -1, -2) == 1; } - int length() const + [[nodiscard]] int length() const { const StackRestore stackRestore(m_L); @@ -9888,7 +9940,7 @@ class LuaRefBase } template - bool append(const Ts&... vs) const + [[nodiscard]] bool append(const Ts&... vs) const { static_assert(sizeof...(vs) > 0); @@ -10015,6 +10067,43 @@ class LuaRef : public LuaRefBase luaL_unref(m_L, LUA_REGISTRYINDEX, m_tableRef); } + bool isValid() const { return m_tableRef != LUA_NOREF; } + + TableItem& operator=(const TableItem& other) + { + if (this == &other) + return *this; + TableItem tmp(other); + swap(tmp); + return *this; + } + + TableItem(TableItem&& other) noexcept + : LuaRefBase(other.m_L) + , m_tableRef(std::exchange(other.m_tableRef, LUA_NOREF)) + , m_keyRef(std::exchange(other.m_keyRef, LUA_NOREF)) + , m_keyLiteral(std::exchange(other.m_keyLiteral, nullptr)) + { + } + + TableItem& operator=(TableItem&& other) noexcept + { + if (this == &other) + return *this; + + if (m_keyRef != LUA_NOREF) + luaL_unref(m_L, LUA_REGISTRYINDEX, m_keyRef); + if (m_tableRef != LUA_NOREF) + luaL_unref(m_L, LUA_REGISTRYINDEX, m_tableRef); + + m_L = other.m_L; + m_tableRef = std::exchange(other.m_tableRef, LUA_NOREF); + m_keyRef = std::exchange(other.m_keyRef, LUA_NOREF); + m_keyLiteral = std::exchange(other.m_keyLiteral, nullptr); + + return *this; + } + template TableItem& operator=(const T& v) { @@ -10177,6 +10266,15 @@ class LuaRef : public LuaRefBase } private: + void swap(TableItem& other) noexcept + { + using std::swap; + swap(m_L, other.m_L); + swap(m_tableRef, other.m_tableRef); + swap(m_keyRef, other.m_keyRef); + swap(m_keyLiteral, other.m_keyLiteral); + } + int m_tableRef = LUA_NOREF; int m_keyRef = LUA_NOREF; const char* m_keyLiteral = nullptr; @@ -10369,15 +10467,21 @@ class LuaRef : public LuaRefBase void moveTo(lua_State* newL) { - push(); + push(); + + lua_xmove(m_L, newL, 1); - lua_xmove(m_L, newL, 1); + const int oldRef = m_ref; + lua_State* const oldL = m_L; m_L = newL; + m_ref = luaL_ref(newL, LUA_REGISTRYINDEX); + + luaL_unref(oldL, LUA_REGISTRYINDEX, oldRef); } template - TableItem operator[](const T& key) const + TableItem operator[](const T& key) const& { if (! Stack::push(m_L, key)) return TableItem(m_L, m_ref); @@ -10385,14 +10489,29 @@ class LuaRef : public LuaRefBase return TableItem(m_L, m_ref); } + template + TableItem operator[](const T& key) && + { + if (! Stack::push(m_L, key)) + return TableItem(m_L, m_ref); + + return TableItem(m_L, std::exchange(m_ref, LUA_NOREF), typename TableItem::AdoptTableRef{}); + } + template - TableItem operator[](const char (&key)[N]) const + TableItem operator[](const char (&key)[N]) const& { return TableItem(m_L, m_ref, key); } + template + TableItem operator[](const char (&key)[N]) && + { + return TableItem(m_L, std::exchange(m_ref, LUA_NOREF), typename TableItem::AdoptTableRef{}, key); + } + template - LuaRef rawget(const T& key) const + [[nodiscard]] LuaRef rawget(const T& key) const { const StackRestore stackRestore(m_L); @@ -10406,7 +10525,7 @@ class LuaRef : public LuaRefBase } template - TypeResult getField(const char* key) const + [[nodiscard]] TypeResult getField(const char* key) const { const StackRestore stackRestore(m_L); @@ -10417,7 +10536,25 @@ class LuaRef : public LuaRefBase } template - bool setField(const char* key, T&& value) const + [[nodiscard]] std::optional tryGetField(const char* key) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + if (! lua_istable(m_L, -1)) + return std::nullopt; + + lua_getfield(m_L, -1, key); + + auto result = Stack>::get(m_L, -1); + if (! result) + return std::nullopt; + + return *result; + } + + template + [[nodiscard]] bool setField(const char* key, T&& value) const { const StackRestore stackRestore(m_L); @@ -10431,7 +10568,7 @@ class LuaRef : public LuaRefBase } template - TypeResult rawgetField(const char* key) const + [[nodiscard]] TypeResult rawgetField(const char* key) const { const StackRestore stackRestore(m_L); @@ -10443,7 +10580,7 @@ class LuaRef : public LuaRefBase } template - bool rawsetField(const char* key, T&& value) const + [[nodiscard]] bool rawsetField(const char* key, T&& value) const { const StackRestore stackRestore(m_L); @@ -10490,39 +10627,54 @@ class LuaRef : public LuaRefBase lua_pop(m_L, 1); } - std::size_t hash() const + [[nodiscard]] std::size_t hash() const { +#if LUABRIDGE_SAFE_STACK_CHECKS + if (! lua_checkstack(m_L, 1)) + return 0; +#endif + + lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_ref); + + const int t = lua_type(m_L, -1); + std::size_t value; - switch (type()) + switch (t) { case LUA_TNONE: + case LUA_TNIL: value = std::hash{}(nullptr); break; case LUA_TBOOLEAN: - value = std::hash{}(unsafe_cast()); + value = std::hash{}(lua_toboolean(m_L, -1) != 0); break; case LUA_TNUMBER: - value = std::hash{}(unsafe_cast()); + value = std::hash{}(lua_tonumber(m_L, -1)); break; case LUA_TSTRING: - value = std::hash{}(unsafe_cast()); + { + std::size_t len = 0; + const char* s = lua_tolstring(m_L, -1, &len); + value = std::hash{}(std::string_view(s, len)); break; + } - case LUA_TNIL: case LUA_TTABLE: case LUA_TFUNCTION: case LUA_TTHREAD: case LUA_TUSERDATA: case LUA_TLIGHTUSERDATA: default: - value = static_cast(m_ref); + value = reinterpret_cast(lua_topointer(m_L, -1)); break; } - const std::size_t seed = std::hash{}(type()); + lua_pop(m_L, 1); + + const std::size_t seed = std::hash{}(t == LUA_TNONE ? LUA_TNIL : t); return value + 0x9e3779b9u + (seed << 6) + (seed >> 2); } @@ -10556,28 +10708,28 @@ template auto operator<(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return !(rhs >= lhs); + return rhs > lhs; } template auto operator<=(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return !(rhs > lhs); + return rhs >= lhs; } template auto operator>(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return rhs <= lhs; + return rhs < lhs; } template auto operator>=(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return !(rhs > lhs); + return rhs <= lhs; } template <> @@ -13186,16 +13338,15 @@ struct Stack> StackRestore stackRestore(L); lua_createtable(L, static_cast(vector.size()), 0); + const int tableIndex = lua_gettop(L); for (std::size_t i = 0; i < vector.size(); ++i) { - lua_pushinteger(L, static_cast(i + 1)); - auto result = Stack::push(L, vector[i]); if (! result) return result; - lua_settable(L, -3); + lua_rawseti(L, tableIndex, i + 1); } stackRestore.reset(); diff --git a/Images/benchmarks.png b/Images/benchmarks.png index f015a46a..660ab8d8 100644 Binary files a/Images/benchmarks.png and b/Images/benchmarks.png differ diff --git a/Source/LuaBridge/Array.h b/Source/LuaBridge/Array.h index 9fe71ab4..fae90244 100644 --- a/Source/LuaBridge/Array.h +++ b/Source/LuaBridge/Array.h @@ -30,16 +30,15 @@ struct Stack> StackRestore stackRestore(L); lua_createtable(L, static_cast(Size), 0); + const int tableIndex = lua_gettop(L); for (std::size_t i = 0; i < Size; ++i) { - lua_pushinteger(L, static_cast(i + 1)); - auto result = Stack::push(L, array[i]); if (! result) return result; - lua_settable(L, -3); + lua_rawseti(L, tableIndex, i + 1); } stackRestore.reset(); diff --git a/Source/LuaBridge/Vector.h b/Source/LuaBridge/Vector.h index 4beb3feb..3fd7da56 100644 --- a/Source/LuaBridge/Vector.h +++ b/Source/LuaBridge/Vector.h @@ -30,16 +30,15 @@ struct Stack> StackRestore stackRestore(L); lua_createtable(L, static_cast(vector.size()), 0); + const int tableIndex = lua_gettop(L); for (std::size_t i = 0; i < vector.size(); ++i) { - lua_pushinteger(L, static_cast(i + 1)); - auto result = Stack::push(L, vector[i]); if (! result) return result; - lua_settable(L, -3); + lua_rawseti(L, tableIndex, i + 1); } stackRestore.reset(); diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index b8621190..a9eded1a 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -1676,6 +1676,31 @@ struct function } }; +template +TypeResult get_member_object(lua_State* L, bool canBeConst) +{ + const int selfIndex = lua_absindex(L, 1); + + if (lua_getmetatable(L, selfIndex)) + { + if (lua_type(L, lua_upvalueindex(2)) == LUA_TTABLE && lua_rawequal(L, -1, lua_upvalueindex(2))) + { + lua_pop(L, 1); + return Userdata::getExactPointer(L, selfIndex); + } + + if (canBeConst && lua_type(L, lua_upvalueindex(3)) == LUA_TTABLE && lua_rawequal(L, -1, lua_upvalueindex(3))) + { + lua_pop(L, 1); + return Userdata::getExactPointer(L, selfIndex); + } + + lua_pop(L, 1); + } + + return Userdata::get(L, selfIndex, canBeConst); +} + //================================================================================================= /** * @brief lua_CFunction to call a class member function with a return value. @@ -1689,7 +1714,7 @@ int invoke_member_function(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - auto ptr = Userdata::get(L, 1, false); + auto ptr = get_member_object(L, false); if (! ptr) raise_lua_error(L, "%s", ptr.error_cstr()); @@ -1706,7 +1731,7 @@ int invoke_const_member_function(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - auto ptr = Userdata::get(L, 1, true); + auto ptr = get_member_object(L, true); if (! ptr) raise_lua_error(L, "%s", ptr.error_cstr()); @@ -2195,7 +2220,8 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...), const c using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); } template @@ -2206,7 +2232,8 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...) noexcept using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &invoke_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); } // Const member function pointer @@ -2218,7 +2245,9 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...) const, c using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getConstRegistryKey()); + lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 3); } template @@ -2229,7 +2258,9 @@ void push_member_function(lua_State* L, ReturnType (U::*mfp)(Params...) const no using F = decltype(mfp); new (lua_newuserdata_x(L, sizeof(F))) F(mfp); - lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getConstRegistryKey()); + lua_pushcclosure_x(L, &detail::invoke_const_member_function, debugname, 3); } // Non const member Lua CFunction pointer diff --git a/Source/LuaBridge/detail/FuncTraits.h b/Source/LuaBridge/detail/FuncTraits.h index f808a7d3..de66b4ab 100644 --- a/Source/LuaBridge/detail/FuncTraits.h +++ b/Source/LuaBridge/detail/FuncTraits.h @@ -821,9 +821,7 @@ auto bind_back(F&& f, BoundArgs&&... args) static constexpr std::size_t num_remaining = num_explicit - num_bound; using explicit_remaining = detail::tuple_take_first_t; - using leading = detail::bind_back_leading_t; - using R = typename FnTraits::result_type; return detail::bind_back_wrapper...>( diff --git a/Source/LuaBridge/detail/Globals.h b/Source/LuaBridge/detail/Globals.h index 51a21bdd..d1fc0db6 100644 --- a/Source/LuaBridge/detail/Globals.h +++ b/Source/LuaBridge/detail/Globals.h @@ -7,6 +7,9 @@ #include "Config.h" #include "Stack.h" +#include +#include + namespace luabridge { //================================================================================================= @@ -27,6 +30,32 @@ TypeResult getGlobal(lua_State* L, const char* name) return result; } +//================================================================================================= +/** + * @brief Try to get a field from a global table without creating a LuaRef. + * + * This is a fast-path helper for optional lookup patterns. It invokes normal Lua field access + * on the table, including metamethods, and returns std::nullopt when the global is not a table + * or the field cannot be converted to the requested type. + */ +template +std::optional tryGetGlobalField(lua_State* L, const char* globalName, const char* fieldName) +{ + const StackRestore stackRestore(L); + + lua_getglobal(L, globalName); + if (! lua_istable(L, -1)) + return std::nullopt; + + lua_getfield(L, -1, fieldName); + + auto result = Stack>::get(L, -1); + if (! result) + return std::nullopt; + + return *result; +} + //================================================================================================= /** * @brief Set a global value in the lua_State. diff --git a/Source/LuaBridge/detail/LuaRef.h b/Source/LuaBridge/detail/LuaRef.h index 7f56495f..575c61d9 100644 --- a/Source/LuaBridge/detail/LuaRef.h +++ b/Source/LuaBridge/detail/LuaRef.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -299,7 +300,7 @@ class LuaRefBase * * @returns An optional string containing the name used to register the class with `beginClass`, nullopt in case it's not a registered class. */ - std::optional getClassName() + std::optional getClassName() const { if (! isUserdata()) return std::nullopt; @@ -556,7 +557,7 @@ class LuaRefBase * @returns True if the referred value is equal to the specified one. */ template - bool rawequal(const T& v) const + [[nodiscard]] bool rawequal(const T& v) const { const StackRestore stackRestore(m_L); @@ -576,7 +577,7 @@ class LuaRefBase * * @returns The length of the referred array. */ - int length() const + [[nodiscard]] int length() const { const StackRestore stackRestore(m_L); @@ -596,7 +597,7 @@ class LuaRefBase * @returns True if all values were successfully appended. */ template - bool append(const Ts&... vs) const + [[nodiscard]] bool append(const Ts&... vs) const { static_assert(sizeof...(vs) > 0); @@ -774,6 +775,77 @@ class LuaRef : public LuaRefBase luaL_unref(m_L, LUA_REGISTRYINDEX, m_tableRef); } + //========================================================================================= + /** + * @brief Indicate whether this TableItem proxy is in a valid state. + * + * @returns True if the table reference is valid, false otherwise. + */ + bool isValid() const { return m_tableRef != LUA_NOREF; } + + //========================================================================================= + /** + * @brief Copy assignment operator (copy-and-swap idiom). + * + * Makes this TableItem refer to the same table slot as `other`. + * + * @param other Another Lua table item reference. + * + * @returns This reference. + */ + TableItem& operator=(const TableItem& other) + { + if (this == &other) + return *this; + TableItem tmp(other); + swap(tmp); + return *this; + } + + //========================================================================================= + /** + * @brief Move constructor. + * + * Transfers ownership of the table and key refs from `other` to this. + * + * @param other Another Lua table item reference (moved-from). + */ + TableItem(TableItem&& other) noexcept + : LuaRefBase(other.m_L) + , m_tableRef(std::exchange(other.m_tableRef, LUA_NOREF)) + , m_keyRef(std::exchange(other.m_keyRef, LUA_NOREF)) + , m_keyLiteral(std::exchange(other.m_keyLiteral, nullptr)) + { + } + + //========================================================================================= + /** + * @brief Move assignment operator. + * + * Releases this TableItem's refs and takes ownership of `other`'s refs. + * + * @param other Another Lua table item reference (moved-from). + * + * @returns This reference. + */ + TableItem& operator=(TableItem&& other) noexcept + { + if (this == &other) + return *this; + + if (m_keyRef != LUA_NOREF) + luaL_unref(m_L, LUA_REGISTRYINDEX, m_keyRef); + if (m_tableRef != LUA_NOREF) + luaL_unref(m_L, LUA_REGISTRYINDEX, m_tableRef); + + m_L = other.m_L; + m_tableRef = std::exchange(other.m_tableRef, LUA_NOREF); + m_keyRef = std::exchange(other.m_keyRef, LUA_NOREF); + m_keyLiteral = std::exchange(other.m_keyLiteral, nullptr); + + return *this; + } + //========================================================================================= /** * @brief Assign a new value to this table key. @@ -1003,6 +1075,15 @@ class LuaRef : public LuaRefBase } private: + void swap(TableItem& other) noexcept + { + using std::swap; + swap(m_L, other.m_L); + swap(m_tableRef, other.m_tableRef); + swap(m_keyRef, other.m_keyRef); + swap(m_keyLiteral, other.m_keyLiteral); + } + int m_tableRef = LUA_NOREF; int m_keyRef = LUA_NOREF; const char* m_keyLiteral = nullptr; @@ -1139,13 +1220,13 @@ class LuaRef : public LuaRefBase //============================================================================================= /** - * @brief Return a reference to a top Lua stack item. + * @brief Return a reference to the top Lua stack item and pop it. * - * The stack item is not popped. + * The stack item is popped. * * @param L A Lua state. * - * @returns A reference to a value on the top of a Lua stack. + * @returns A reference to a value that was on the top of the Lua stack. */ static LuaRef fromStack(lua_State* L) { @@ -1367,11 +1448,17 @@ class LuaRef : public LuaRefBase */ void moveTo(lua_State* newL) { - push(); + push(); // push value onto m_L + + lua_xmove(m_L, newL, 1); // move value from m_L to newL (pops m_L, pushes newL) - lua_xmove(m_L, newL, 1); + const int oldRef = m_ref; + lua_State* const oldL = m_L; m_L = newL; + m_ref = luaL_ref(newL, LUA_REGISTRYINDEX); // register on newL (pops from newL's stack) + + luaL_unref(oldL, LUA_REGISTRYINDEX, oldRef); // release old registry entry } //============================================================================================= @@ -1385,7 +1472,7 @@ class LuaRef : public LuaRefBase * @returns A reference to the table item. */ template - TableItem operator[](const T& key) const + TableItem operator[](const T& key) const& { if (! Stack::push(m_L, key)) return TableItem(m_L, m_ref); @@ -1393,12 +1480,27 @@ class LuaRef : public LuaRefBase return TableItem(m_L, m_ref); } + template + TableItem operator[](const T& key) && + { + if (! Stack::push(m_L, key)) + return TableItem(m_L, m_ref); + + return TableItem(m_L, std::exchange(m_ref, LUA_NOREF), typename TableItem::AdoptTableRef{}); + } + template - TableItem operator[](const char (&key)[N]) const + TableItem operator[](const char (&key)[N]) const& { return TableItem(m_L, m_ref, key); } + template + TableItem operator[](const char (&key)[N]) && + { + return TableItem(m_L, std::exchange(m_ref, LUA_NOREF), typename TableItem::AdoptTableRef{}, key); + } + //============================================================================================= /** * @brief Access a table value using a key. @@ -1410,7 +1512,7 @@ class LuaRef : public LuaRefBase * @returns A reference to the table item. */ template - LuaRef rawget(const T& key) const + [[nodiscard]] LuaRef rawget(const T& key) const { const StackRestore stackRestore(m_L); @@ -1435,7 +1537,7 @@ class LuaRef : public LuaRefBase * @returns A converted value or an error. */ template - TypeResult getField(const char* key) const + [[nodiscard]] TypeResult getField(const char* key) const { const StackRestore stackRestore(m_L); @@ -1445,6 +1547,31 @@ class LuaRef : public LuaRefBase return Stack::get(m_L, -1); } + //============================================================================================= + /** + * @brief Try to get a table field by C-string key and convert to T. + * + * This invokes metamethods and returns std::nullopt when the referred value is not a table + * or the field cannot be converted to the requested type. + */ + template + [[nodiscard]] std::optional tryGetField(const char* key) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + if (! lua_istable(m_L, -1)) + return std::nullopt; + + lua_getfield(m_L, -1, key); + + auto result = Stack>::get(m_L, -1); + if (! result) + return std::nullopt; + + return *result; + } + //============================================================================================= /** * @brief Set a table field by C-string key. @@ -1457,7 +1584,7 @@ class LuaRef : public LuaRefBase * @returns True if value push succeeded, false otherwise. */ template - bool setField(const char* key, T&& value) const + [[nodiscard]] bool setField(const char* key, T&& value) const { const StackRestore stackRestore(m_L); @@ -1479,7 +1606,7 @@ class LuaRef : public LuaRefBase * @returns A converted value or an error. */ template - TypeResult rawgetField(const char* key) const + [[nodiscard]] TypeResult rawgetField(const char* key) const { const StackRestore stackRestore(m_L); @@ -1500,7 +1627,7 @@ class LuaRef : public LuaRefBase * @returns True if key/value push succeeded, false otherwise. */ template - bool rawsetField(const char* key, T&& value) const + [[nodiscard]] bool rawsetField(const char* key, T&& value) const { const StackRestore stackRestore(m_L); @@ -1568,39 +1695,54 @@ class LuaRef : public LuaRefBase /** * @brief Get the unique hash of a LuaRef. */ - std::size_t hash() const + [[nodiscard]] std::size_t hash() const { +#if LUABRIDGE_SAFE_STACK_CHECKS + if (! lua_checkstack(m_L, 1)) + return 0; +#endif + + lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_ref); + + const int t = lua_type(m_L, -1); + std::size_t value; - switch (type()) + switch (t) { case LUA_TNONE: + case LUA_TNIL: value = std::hash{}(nullptr); break; case LUA_TBOOLEAN: - value = std::hash{}(unsafe_cast()); + value = std::hash{}(lua_toboolean(m_L, -1) != 0); break; case LUA_TNUMBER: - value = std::hash{}(unsafe_cast()); + value = std::hash{}(lua_tonumber(m_L, -1)); break; case LUA_TSTRING: - value = std::hash{}(unsafe_cast()); + { + std::size_t len = 0; + const char* s = lua_tolstring(m_L, -1, &len); + value = std::hash{}(std::string_view(s, len)); break; + } - case LUA_TNIL: case LUA_TTABLE: case LUA_TFUNCTION: case LUA_TTHREAD: case LUA_TUSERDATA: case LUA_TLIGHTUSERDATA: default: - value = static_cast(m_ref); + value = reinterpret_cast(lua_topointer(m_L, -1)); break; } - const std::size_t seed = std::hash{}(type()); + lua_pop(m_L, 1); + + const std::size_t seed = std::hash{}(t == LUA_TNONE ? LUA_TNIL : t); return value + 0x9e3779b9u + (seed << 6) + (seed >> 2); } @@ -1644,7 +1786,7 @@ template auto operator<(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return !(rhs >= lhs); + return rhs > lhs; } /** @@ -1654,7 +1796,7 @@ template auto operator<=(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return !(rhs > lhs); + return rhs >= lhs; } /** @@ -1664,7 +1806,7 @@ template auto operator>(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return rhs <= lhs; + return rhs < lhs; } /** @@ -1674,7 +1816,7 @@ template auto operator>=(const T& lhs, const LuaRef& rhs) -> std::enable_if_t && !std::is_same_v>, bool> { - return !(rhs > lhs); + return rhs <= lhs; } //================================================================================================= diff --git a/Source/LuaBridge/detail/Userdata.h b/Source/LuaBridge/detail/Userdata.h index 35e6aa3f..e02b49ec 100644 --- a/Source/LuaBridge/detail/Userdata.h +++ b/Source/LuaBridge/detail/Userdata.h @@ -354,6 +354,12 @@ class Userdata return static_cast(rawPtr); } + template + static T* getExactPointer(lua_State* L, int index) noexcept + { + return static_cast(static_cast(lua_touserdata(L, index))->getPointer()); + } + template static bool isInstance(lua_State* L, int index) { diff --git a/Tests/Source/LuaRefTests.cpp b/Tests/Source/LuaRefTests.cpp index 72fd8f12..d8dba94b 100644 --- a/Tests/Source/LuaRefTests.cpp +++ b/Tests/Source/LuaRefTests.cpp @@ -1059,6 +1059,50 @@ TEST_F(LuaRefTests, FieldHelpersRespectMetamethodsAndRawAccess) EXPECT_EQ(99, *rawValue); } +TEST_F(LuaRefTests, TryGetFieldReturnsOptionalAndKeepsStackBalanced) +{ + runLua("indexCalls = 0 " + "result = setmetatable({ value = 42 }, {" + " __index = function(_, key) indexCalls = indexCalls + 1; if key == 'metaValue' then return 84 end end" + "})"); + + auto table = result(); + + const int topBefore = lua_gettop(L); + EXPECT_EQ(42, *table.tryGetField("value")); + EXPECT_EQ(topBefore, lua_gettop(L)); + + EXPECT_EQ(84, *table.tryGetField("metaValue")); + EXPECT_EQ(1, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); + + EXPECT_FALSE(table.tryGetField("missing")); + EXPECT_FALSE(table.tryGetField("metaValueTypo")); + + runLua("result = 5"); + EXPECT_FALSE(result().tryGetField("value")); + EXPECT_EQ(topBefore, lua_gettop(L)); +} + +TEST_F(LuaRefTests, TryGetGlobalFieldReturnsOptionalAndKeepsStackBalanced) +{ + runLua("indexCalls = 0 " + "warble = setmetatable({ value = 24.0, text = 'x' }, {" + " __index = function(_, key) indexCalls = indexCalls + 1; if key == 'metaValue' then return 48.0 end end" + "})"); + + const int topBefore = lua_gettop(L); + EXPECT_EQ(24.0, *luabridge::tryGetGlobalField(L, "warble", "value")); + EXPECT_EQ(topBefore, lua_gettop(L)); + + EXPECT_EQ(48.0, *luabridge::tryGetGlobalField(L, "warble", "metaValue")); + EXPECT_EQ(1, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); + + EXPECT_FALSE(luabridge::tryGetGlobalField(L, "warble", "text")); + EXPECT_FALSE(luabridge::tryGetGlobalField(L, "warble", "missing")); + EXPECT_FALSE(luabridge::tryGetGlobalField(L, "missingGlobal", "value")); + EXPECT_EQ(topBefore, lua_gettop(L)); +} + TEST_F(LuaRefTests, UnsafeRawFieldHelpersKeepStackBalanced) { runLua("indexCalls = 0 " @@ -1515,3 +1559,353 @@ TEST_F(LuaRefTests, LuaRefConstructorPushFailure) EXPECT_FALSE(ref.isValid()); } } + +TEST_F(LuaRefTests, NonMemberComparisonCorrectness) +{ + luabridge::LuaRef seven(L, 7); + luabridge::LuaRef eight(L, 8); + + // Equal values: T op LuaRef + EXPECT_FALSE(7 < seven); + EXPECT_TRUE(7 <= seven); + EXPECT_FALSE(7 > seven); + EXPECT_TRUE(7 >= seven); + + // lhs < rhs: T op LuaRef + EXPECT_TRUE(7 < eight); + EXPECT_TRUE(7 <= eight); + EXPECT_FALSE(7 > eight); + EXPECT_FALSE(7 >= eight); + + // lhs > rhs: T op LuaRef + EXPECT_FALSE(8 < seven); + EXPECT_FALSE(8 <= seven); + EXPECT_TRUE(8 > seven); + EXPECT_TRUE(8 >= seven); +} + +TEST_F(LuaRefTests, HashContract) +{ + // Invalid ref and nil ref compare equal — must hash equal + luabridge::LuaRef invalid(L); + luabridge::LuaRef nil(L, luabridge::LuaNil()); + EXPECT_TRUE(invalid == nil); + EXPECT_EQ(invalid.hash(), nil.hash()); + + // Two nil refs must hash equal + luabridge::LuaRef nil2(L, luabridge::LuaNil()); + EXPECT_EQ(nil.hash(), nil2.hash()); + + // Copies of a table ref must hash equal (same object identity) + runLua("result = {}"); + luabridge::LuaRef t1 = result(); + luabridge::LuaRef t2 = result(); + EXPECT_TRUE(t1 == t2); + EXPECT_EQ(t1.hash(), t2.hash()); + + // Copies of a function ref must hash equal + runLua("result = function() end"); + luabridge::LuaRef f1 = result(); + luabridge::LuaRef f2 = result(); + EXPECT_EQ(f1.hash(), f2.hash()); + + // Strings with same content must hash equal + luabridge::LuaRef s1(L, "hello"); + luabridge::LuaRef s2(L, "hello"); + EXPECT_TRUE(s1 == s2); + EXPECT_EQ(s1.hash(), s2.hash()); + + // Strings with different content must hash differently (very high probability) + luabridge::LuaRef s3(L, "world"); + EXPECT_NE(s1.hash(), s3.hash()); +} + +TEST_F(LuaRefTests, TableItemCopyAssignmentAndMove) +{ + runLua("result = { a = 10, b = 20 }"); + + auto a = result()["a"]; + auto b = result()["b"]; + + EXPECT_EQ(10, luabridge::cast(luabridge::LuaRef(a)).valueOr(0)); + EXPECT_EQ(20, luabridge::cast(luabridge::LuaRef(b)).valueOr(0)); + + // Copy assignment: b = a (b now refers to same slot as a) + b = a; + EXPECT_EQ(10, luabridge::cast(luabridge::LuaRef(b)).valueOr(0)); + + // Move constructor + auto c = std::move(b); + EXPECT_EQ(10, luabridge::cast(luabridge::LuaRef(c)).valueOr(0)); + + // Move assignment + runLua("result = { x = 99 }"); + auto x = result()["x"]; + auto y = result()["x"]; + y = std::move(x); + EXPECT_EQ(99, luabridge::cast(luabridge::LuaRef(y)).valueOr(0)); +} + +TEST_F(LuaRefTests, GetClassNameConst) +{ + struct MyClass {}; + luabridge::getGlobalNamespace(L) + .beginClass("MyClass") + .addConstructor() + .endClass(); + + runLua("result = MyClass()"); + const luabridge::LuaRef constRef = result(); + EXPECT_EQ("MyClass", constRef.getClassName().value_or("")); +} + +TEST_F(LuaRefTests, MoveToStackBalance) +{ + runLua("result = 42"); + luabridge::LuaRef ref = result(); + EXPECT_TRUE(ref.isNumber()); + EXPECT_EQ(42, ref.cast().valueOr(0)); + + const int mainTopBefore = lua_gettop(L); + + lua_State* thread = lua_newthread(L); + const int threadTop = lua_gettop(thread); + + ref.moveTo(thread); + + // Main state: only the thread object was added by lua_newthread + EXPECT_EQ(mainTopBefore + 1, lua_gettop(L)); + + // Thread stack must be balanced after moveTo (luaL_ref consumes the xmoved value) + EXPECT_EQ(threadTop, lua_gettop(thread)); + + // The ref now lives on the thread state with the correct value + EXPECT_TRUE(ref.isNumber()); + EXPECT_EQ(42, ref.cast().valueOr(0)); +} + +TEST_F(LuaRefTests, TableItemIsValid) +{ + runLua("result = { key = 1 }"); + + // Valid TableItem from string literal key + auto item = result()["key"]; + EXPECT_TRUE(item.isValid()); + + // Valid TableItem from dynamic key + std::string dynKey = "key"; + auto item2 = result()[dynKey]; + EXPECT_TRUE(item2.isValid()); + + // After move: target is valid, source is invalid + auto moved = std::move(item2); + EXPECT_TRUE(moved.isValid()); + EXPECT_FALSE(item2.isValid()); + + // Copy-constructed and copy-assigned TableItems are valid + auto copied = item; + EXPECT_TRUE(item.isValid()); + EXPECT_TRUE(copied.isValid()); +} + +// --- NonMemberComparison additional coverage --- + +TEST_F(LuaRefTests, NonMemberComparisonStrings) +{ + luabridge::LuaRef hello(L, "hello"); + luabridge::LuaRef world(L, "world"); + + // "hello" < "world" lexicographically + EXPECT_TRUE(std::string("hello") < world); + EXPECT_TRUE(std::string("hello") <= world); + EXPECT_FALSE(std::string("hello") > world); + EXPECT_FALSE(std::string("hello") >= world); + + // Equal string values + EXPECT_FALSE(std::string("hello") < hello); + EXPECT_TRUE(std::string("hello") <= hello); + EXPECT_FALSE(std::string("hello") > hello); + EXPECT_TRUE(std::string("hello") >= hello); + + EXPECT_FALSE(std::string("world") < world); + EXPECT_TRUE(std::string("world") <= world); + EXPECT_FALSE(std::string("world") > world); + EXPECT_TRUE(std::string("world") >= world); +} + +TEST_F(LuaRefTests, NonMemberComparisonSymmetry) +{ + luabridge::LuaRef seven(L, 7); + luabridge::LuaRef eight(L, 8); + + // (T op LuaRef) must produce the same answer as (LuaRef reverse-op T) + EXPECT_EQ((7 < eight), static_cast(eight > 7)); + EXPECT_EQ((7 <= eight), static_cast(eight >= 7)); + EXPECT_EQ((8 > seven), static_cast(seven < 8)); + EXPECT_EQ((8 >= seven), static_cast(seven <= 8)); + + // Equal values + EXPECT_EQ((7 < seven), static_cast(seven > 7)); + EXPECT_EQ((7 <= seven), static_cast(seven >= 7)); + EXPECT_EQ((7 > seven), static_cast(seven < 7)); + EXPECT_EQ((7 >= seven), static_cast(seven <= 7)); +} + +TEST_F(LuaRefTests, NonMemberEqualityAndInequality) +{ + luabridge::LuaRef seven(L, 7); + luabridge::LuaRef eight(L, 8); + + EXPECT_TRUE(7 == seven); + EXPECT_FALSE(7 == eight); + EXPECT_FALSE(8 == seven); + EXPECT_TRUE(8 == eight); + + EXPECT_FALSE(7 != seven); + EXPECT_TRUE(7 != eight); +} + +// --- Hash additional coverage --- + +TEST_F(LuaRefTests, HashBooleans) +{ + luabridge::LuaRef t1(L, true); + luabridge::LuaRef t2(L, true); + luabridge::LuaRef f1(L, false); + + EXPECT_EQ(t1.hash(), t2.hash()); + EXPECT_NE(t1.hash(), f1.hash()); +} + +TEST_F(LuaRefTests, HashNumbers) +{ + luabridge::LuaRef a(L, 42); + luabridge::LuaRef b(L, 42); + luabridge::LuaRef c(L, 43); + + EXPECT_EQ(a.hash(), b.hash()); + EXPECT_NE(a.hash(), c.hash()); +} + +TEST_F(LuaRefTests, HashDistinctTablesDistinct) +{ + runLua("hashT1 = {}; hashT2 = {}"); + auto t1 = luabridge::getGlobal(L, "hashT1"); + auto t2 = luabridge::getGlobal(L, "hashT2"); + + EXPECT_FALSE(t1 == t2); + EXPECT_NE(t1.hash(), t2.hash()); +} + +TEST_F(LuaRefTests, HashUsableInUnorderedMap) +{ + std::unordered_map map; + + luabridge::LuaRef key1(L, "mapkey1"); + luabridge::LuaRef key2(L, "mapkey2"); + + map[key1] = 100; + map[key2] = 200; + + luabridge::LuaRef key1Copy(L, "mapkey1"); + EXPECT_EQ(100, map.at(key1Copy)); + EXPECT_EQ(200, map.at(key2)); +} + +// --- TableItem copy/move additional coverage --- + +TEST_F(LuaRefTests, TableItemCopyAssignmentSelfAssign) +{ + runLua("result = { a = 42 }"); + auto item = result()["a"]; + EXPECT_TRUE(item.isValid()); + +#if __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wself-assign-overloaded" +#endif + item = item; +#if __clang__ +#pragma clang diagnostic pop +#endif + + EXPECT_TRUE(item.isValid()); + EXPECT_EQ(42, item.unsafe_cast()); +} + +TEST_F(LuaRefTests, TableItemCopyAssignmentSharesSlot) +{ + // After b = a, b proxies the same table slot as a. + // Writing through b must also be visible when reading through a. + runLua("result = { a = 1, b = 2 }"); + auto table = result(); + + auto a = table["a"]; + auto b = table["b"]; + + b = a; + EXPECT_EQ(1, luabridge::LuaRef(b).unsafe_cast()); + + b = 99; + EXPECT_EQ(99, table["a"].unsafe_cast()); + EXPECT_EQ(99, luabridge::LuaRef(a).unsafe_cast()); +} + +TEST_F(LuaRefTests, TableItemMovedFromDestructible) +{ + runLua("result = { key = 1 }"); + auto item = result()["key"]; + + auto moved = std::move(item); + + EXPECT_FALSE(item.isValid()); + EXPECT_TRUE(moved.isValid()); + // item goes out of scope here — must not double-unref or crash +} + +TEST_F(LuaRefTests, TableItemMoveAssignReleasesOldRefs) +{ + runLua("result = { a = 10, b = 20 }"); + auto a = result()["a"]; + auto b = result()["b"]; + + b = std::move(a); + + EXPECT_FALSE(a.isValid()); + EXPECT_TRUE(b.isValid()); + EXPECT_EQ(10, luabridge::LuaRef(b).unsafe_cast()); +} + +// --- moveTo() additional coverage --- + +TEST_F(LuaRefTests, MoveToTransfersString) +{ + runLua("result = 'hello'"); + luabridge::LuaRef ref = result(); + EXPECT_TRUE(ref.isString()); + + lua_State* thread = lua_newthread(L); + const int threadTop = lua_gettop(thread); + + ref.moveTo(thread); + + EXPECT_EQ(threadTop, lua_gettop(thread)); + EXPECT_TRUE(ref.isString()); + EXPECT_EQ("hello", ref.cast().valueOr("")); +} + +TEST_F(LuaRefTests, MoveToNilRef) +{ + luabridge::LuaRef nilRef(L, luabridge::LuaNil()); + EXPECT_TRUE(nilRef.isNil()); + + lua_State* thread = lua_newthread(L); + const int mainTop = lua_gettop(L); + const int threadTop = lua_gettop(thread); + + nilRef.moveTo(thread); + + EXPECT_EQ(mainTop, lua_gettop(L)); + EXPECT_EQ(threadTop, lua_gettop(thread)); + EXPECT_TRUE(nilRef.isNil()); +}