From 396446f11dea36070a7f6d57dd8fe932b6ff730c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 28 Mar 2026 14:13:22 +0100 Subject: [PATCH 1/3] Upload cobertura.xml --- .github/workflows/coverage.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 59fce3d8..dbb4e45f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -477,6 +477,19 @@ jobs: -a "coverage/ravi.info" \ -o "coverage/merged.info" + - name: Install lcov2xml + run: cargo install lcov2xml + + - name: Convert to Cobertura XML + working-directory: ${{runner.workspace}}/build + run: lcov2xml coverage/merged.info -o coverage/cobertura.xml + + - name: Upload Cobertura XML + uses: actions/upload-artifact@v4 + with: + name: cobertura-coverage + path: ${{runner.workspace}}/build/coverage/cobertura.xml + - name: Coveralls uses: coverallsapp/github-action@master with: From fb0163f9a99ba4af03d7ee15d1cead002d7b3131 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 30 Mar 2026 08:51:56 +0200 Subject: [PATCH 2/3] Multiple inheritance --- Source/LuaBridge/detail/CFunctions.h | 502 ++++++++++++----- Source/LuaBridge/detail/Namespace.h | 141 ++++- Source/LuaBridge/detail/Userdata.h | 70 ++- Tests/CMakeLists.txt | 1 + Tests/Source/MultipleInheritanceTests.cpp | 630 ++++++++++++++++++++++ 5 files changed, 1175 insertions(+), 169 deletions(-) create mode 100644 Tests/Source/MultipleInheritanceTests.cpp diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index dcd98e57..3d7b33d5 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -288,11 +288,78 @@ inline std::optional try_call_index_extensible(lua_State* L, const char* ke return std::nullopt; } +template +inline std::optional try_call_parent_index_fallback(lua_State* L, const char* key) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt + + if (key == nullptr) + return std::nullopt; + + lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent list | nil + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); // Stack: mt + return std::nullopt; + } + + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); // Stack: mt, parent list, parent mt + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; + } + + const Options parentOptions = get_class_options(L, -1); // Stack: mt, parent list, parent mt + if (parentOptions.test(extensibleClass | ~allowOverridingMethods)) + { + if (auto result = try_call_index_extensible(L, key)) + { + lua_remove(L, -2); // Stack: mt, result + lua_remove(L, -2); // Stack: result + return *result; + } + } + + lua_rawgetp_x(L, -1, getIndexFallbackKey()); // Stack: mt, parent list, parent mt, ifb | nil + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); // Stack: mt, parent list, parent mt, ifb, arg1 + lua_pushvalue(L, 2); // Stack: mt, parent list, parent mt, ifb, arg1, arg2 + lua_call(L, 2, 1); // Stack: mt, parent list, parent mt, result + + if (! lua_isnoneornil(L, -1)) + { + lua_remove(L, -2); // Stack: mt, parent list, result + lua_remove(L, -2); // Stack: mt, result + lua_remove(L, -2); // Stack: result + return 1; + } + + lua_pop(L, 1); // Stack: mt, parent list, parent mt + } + else + { + lua_pop(L, 1); // Stack: mt, parent list, parent mt + } + + lua_pop(L, 1); // Stack: mt, parent list + } + + lua_pop(L, 1); // Stack: mt + return std::nullopt; +} + template inline int index_metamethod(lua_State* L) { #if LUABRIDGE_SAFE_STACK_CHECKS - luaL_checkstack(L, 3, detail::error_lua_stack_overflow); + luaL_checkstack(L, 6, detail::error_lua_stack_overflow); #endif LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); // Stack (further not shown): table | userdata, name @@ -386,44 +453,81 @@ inline int index_metamethod(lua_State* L) // It may mean that the field may be in const table and it's constness violation. - // Repeat the lookup in the parent metafield, or fallback to extensible class check. - lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent mt | nil - if (lua_isnil(L, -1)) // Stack: mt, nil + // Search flattened parent list in declaration-order DFS. + lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent list | nil + + if (lua_istable(L, -1)) { - lua_pop(L, 2); // Stack: - - break; + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); // Stack: mt, parent list, parent mt + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; + } + + lua_pushvalue(L, 2); // Stack: mt, parent list, parent mt, field name + lua_rawget(L, -2); // Stack: mt, parent list, parent mt, field | nil + if (! lua_isnil(L, -1)) + { + lua_remove(L, -2); // Stack: mt, parent list, field + lua_remove(L, -2); // Stack: mt, field + lua_remove(L, -2); // Stack: field + return 1; + } + lua_pop(L, 1); // Stack: mt, parent list, parent mt + + lua_rawgetp_x(L, -1, getPropgetKey()); // Stack: mt, parent list, parent mt, pg | nil + if (lua_istable(L, -1)) + { + lua_pushvalue(L, 2); // Stack: mt, parent list, parent mt, pg, field name + lua_rawget(L, -2); // Stack: mt, parent list, parent mt, pg, getter | nil + lua_remove(L, -2); // Stack: mt, parent list, parent mt, getter | nil + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); // Stack: mt, parent list, getter + lua_remove(L, -2); // Stack: mt, getter + lua_remove(L, -2); // Stack: getter + lua_pushvalue(L, 1); // Stack: getter, table | userdata + lua_call(L, 1, 1); // Stack: value + return 1; + } + + lua_pop(L, 1); // Stack: mt, parent list, parent mt + } + else + { + lua_pop(L, 1); // Stack: mt, parent list, parent mt + } + + lua_pop(L, 1); // Stack: mt, parent list + } } - // Remove the metatable and repeat the search in the parent one. - LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, parent mt - lua_remove(L, -2); // Stack: parent mt + lua_pop(L, 2); // Stack: - + break; } lua_getmetatable(L, 1); // Stack: class/const table (mt) LUABRIDGE_ASSERT(lua_istable(L, -1)); - for (;;) + const Options options = get_class_options(L, -1); // Stack: mt + if (options.test(extensibleClass | ~allowOverridingMethods)) { - const Options options = get_class_options(L, -1); // Stack: mt - - if (options.test(extensibleClass | ~allowOverridingMethods)) - { - if (auto result = try_call_index_extensible(L, key)) - return *result; - } + if (auto result = try_call_index_extensible(L, key)) + return *result; + } - // Repeat the lookup in the parent metafield, or return nil if the field doesn't exist. - lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent mt | nil - if (lua_isnil(L, -1)) // Stack: mt, nil - { - lua_remove(L, -2); // Stack: nil - return 1; - } + if (auto result = try_call_parent_index_fallback(L, key)) + return *result; - // Remove the metatable and repeat the search in the parent one. - LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, parent mt - lua_remove(L, -2); // Stack: parent mt - } + lua_pop(L, 1); // Stack: - + lua_pushnil(L); + return 1; // no return } @@ -682,53 +786,69 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* } // Stack: mt, orig_ct - lua_pushvalue(L, -2); // Stack: mt, orig_ct, cur_mt (traversal copy) + const int mtIndex = lua_absindex(L, -2); + const int origClassTableIndex = lua_absindex(L, -1); + const std::string superMethodName = make_super_method_name(key); - for (;;) + const auto process_metatable = [L, key, &superMethodName, origClassTableIndex](int candidateMtIndex) { - push_class_or_const_table(L, -1); // Stack: mt, orig_ct, cur_mt, cur_ct | nil - if (! lua_istable(L, -1)) // Stack: mt, orig_ct, cur_mt, nil + push_class_or_const_table(L, candidateMtIndex); // Stack: ..., candidate_ct | nil + if (! lua_istable(L, -1)) { - lua_pop(L, 2); // Stack: mt, orig_ct - break; + lua_pop(L, 1); + return false; } - lua_pushvalue(L, 2); // Stack: mt, orig_ct, cur_mt, cur_ct, field name - lua_rawget(L, -2); // Stack: mt, orig_ct, cur_mt, cur_ct, field | nil + lua_pushvalue(L, 2); // Stack: ..., candidate_ct, field name + lua_rawget(L, -2); // Stack: ..., candidate_ct, field | nil - if (! lua_isnil(L, -1)) // Stack: mt, orig_ct, cur_mt, cur_ct, field + if (lua_isnil(L, -1)) { - if (! lua_iscfunction(L, -1)) - { - lua_pop(L, 3); // Stack: mt, orig_ct - break; - } - - const Options options = get_class_options(L, -2); // Stack: mt, orig_ct, cur_mt, cur_ct, field - if (! options.test(allowOverridingMethods)) - luaL_error(L, "immutable member '%s'", key); + lua_pop(L, 2); // Stack: ... + return false; + } - // Store super_ alias in the ORIGINAL (derived) class table so only derived - // instances can call it; the base class table is left completely untouched. - rawsetfield(L, -4, make_super_method_name(key).c_str()); // Stack: mt, orig_ct, cur_mt, cur_ct - lua_pop(L, 2); // Stack: mt, orig_ct - break; + if (! lua_iscfunction(L, -1)) + { + lua_pop(L, 2); // Stack: ... + return true; } - lua_pop(L, 1); // Stack: mt, orig_ct, cur_mt, cur_ct + const Options options = get_class_options(L, -2); + if (! options.test(allowOverridingMethods)) + luaL_error(L, "immutable member '%s'", key); + + rawsetfield(L, origClassTableIndex, superMethodName.c_str()); // Stack: ..., candidate_ct + lua_pop(L, 1); // Stack: ... + return true; + }; - lua_rawgetp_x(L, -2, getParentKey()); // Stack: mt, orig_ct, cur_mt, cur_ct, pmt | nil - if (lua_isnil(L, -1)) // Stack: mt, orig_ct, cur_mt, cur_ct, nil + (void) process_metatable(mtIndex); + + lua_rawgetp_x(L, mtIndex, getParentKey()); // Stack: mt, orig_ct, parent list | nil + if (lua_istable(L, -1)) + { + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) { - lua_pop(L, 3); // Stack: mt, orig_ct - break; - } + lua_rawgeti(L, parentListIndex, i); // Stack: mt, orig_ct, parent list, parent mt + if (lua_istable(L, -1)) + { + if (process_metatable(lua_absindex(L, -1))) + { + lua_pop(L, 1); // Stack: mt, orig_ct, parent list + break; + } + } - LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, orig_ct, cur_mt, cur_ct, pmt - lua_remove(L, -2); // Stack: mt, orig_ct, cur_mt, pmt - lua_remove(L, -2); // Stack: mt, orig_ct, pmt + lua_pop(L, 1); // Stack: mt, orig_ct, parent list + } } + lua_pop(L, 1); // Stack: mt, orig_ct + // Stack: mt, orig_ct — write to the original (most-derived) class table. lua_getmetatable(L, -1); // Stack: mt, orig_ct, orig_ct_mt lua_pushvalue(L, 3); // Stack: mt, orig_ct, orig_ct_mt, arg3 @@ -738,63 +858,168 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* } // Non-function value (static property): use original traversal-end write location. - lua_pushvalue(L, -1); // Stack: mt, mt + const int rootMetatableIndex = lua_absindex(L, -1); + lua_pushvalue(L, rootMetatableIndex); // Stack: mt, target mt + const int targetMetatableIndex = lua_absindex(L, -1); - for (;;) + const auto process_metatable = [L, key, targetMetatableIndex](int candidateMtIndex) { - push_class_or_const_table(L, -1); // Stack: mt, mt, ct | nil - if (! lua_istable(L, -1)) // Stack: mt, mt, nil + push_class_or_const_table(L, candidateMtIndex); // Stack: ..., candidate_ct | nil + if (! lua_istable(L, -1)) { - lua_pop(L, 2); // Stack: mt - return std::nullopt; + lua_pop(L, 1); + return false; } - lua_pushvalue(L, 2); // Stack: mt, mt, ct, field name - lua_rawget(L, -2); // Stack: mt, mt, ct, field | nil + lua_pushvalue(L, 2); // Stack: ..., candidate_ct, field name + lua_rawget(L, -2); // Stack: ..., candidate_ct, field | nil - if (! lua_isnil(L, -1)) // Stack: mt, mt, ct, field + if (lua_isnil(L, -1)) { - if (! lua_iscfunction(L, -1)) + lua_pop(L, 2); // Stack: ... + return false; + } + + lua_pushvalue(L, candidateMtIndex); + lua_replace(L, targetMetatableIndex); + + if (! lua_iscfunction(L, -1)) + { + lua_pop(L, 2); // Stack: ... + return true; + } + + const Options options = get_class_options(L, -2); + if (! options.test(allowOverridingMethods)) + luaL_error(L, "immutable member '%s'", key); + + rawsetfield(L, -2, make_super_method_name(key).c_str()); // Stack: ..., candidate_ct + lua_pop(L, 1); // Stack: ... + return true; + }; + + (void) process_metatable(rootMetatableIndex); + + lua_rawgetp_x(L, rootMetatableIndex, getParentKey()); // Stack: mt, target mt, parent list | nil + if (lua_istable(L, -1)) + { + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); // Stack: mt, target mt, parent list, parent mt + if (! lua_istable(L, -1)) { lua_pop(L, 1); + continue; + } + + // Preserve old traversal-end behavior when no member exists in the chain. + lua_pushvalue(L, -1); + lua_replace(L, targetMetatableIndex); + + if (process_metatable(lua_absindex(L, -1))) + { + lua_pop(L, 1); // Stack: mt, target mt, parent list break; } - const Options options = get_class_options(L, -2); // Stack: mt, mt, ct, field - if (! options.test(allowOverridingMethods)) - luaL_error(L, "immutable member '%s'", key); + lua_pop(L, 1); // Stack: mt, target mt, parent list + } + } + + lua_pop(L, 1); // Stack: mt, target mt + + push_class_or_const_table(L, targetMetatableIndex); // Stack: mt, target mt, ct | nil + if (! lua_istable(L, -1)) // Stack: mt, target mt, nil + { + lua_pop(L, 2); // Stack: mt + return std::nullopt; + } + + lua_getmetatable(L, -1); // Stack: mt, target mt, ct, ct_mt + lua_pushvalue(L, 3); // Stack: mt, target mt, ct, ct_mt, arg3 + rawsetfield(L, -2, key); // Stack: mt, target mt, ct, ct_mt + lua_pop(L, 3); // Stack: mt + return 0; +} + +template +inline std::optional try_call_parent_newindex(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt - rawsetfield(L, -2, make_super_method_name(key).c_str()); // Stack: mt, mt, ct - break; + lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent list | nil + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); // Stack: mt + return std::nullopt; + } + + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); + + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, parentListIndex, i); // Stack: mt, parent list, parent mt + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + continue; } - lua_pop(L, 1); // Stack: mt, mt, ct + lua_rawgetp_x(L, -1, getPropsetKey()); // Stack: mt, parent list, parent mt, ps | nil + if (lua_istable(L, -1)) + { + lua_pushvalue(L, 2); // Stack: mt, parent list, parent mt, ps, field name + lua_rawget(L, -2); // Stack: mt, parent list, parent mt, ps, setter | nil + lua_remove(L, -2); // Stack: mt, parent list, parent mt, setter | nil - lua_rawgetp_x(L, -2, getParentKey()); // Stack: mt, mt, ct, pmt | nil - if (lua_isnil(L, -1)) // Stack: mt, mt, ct, nil + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); // Stack: mt, parent list, setter + lua_remove(L, -2); // Stack: mt, setter + lua_remove(L, -2); // Stack: setter + + if constexpr (IsObject) + lua_pushvalue(L, 1); // Stack: setter, table | userdata + lua_pushvalue(L, 3); // Stack: setter, table | userdata, new value + lua_call(L, IsObject ? 2 : 1, 0); // Stack: - + return 0; + } + + lua_pop(L, 1); // Stack: mt, parent list, parent mt + } + else { - lua_pop(L, 1); // Stack: mt, mt, ct - break; + lua_pop(L, 1); // Stack: mt, parent list, parent mt } - LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, mt, ct, pmt - lua_remove(L, -2); // Stack: mt, mt, pmt - lua_remove(L, -2); // Stack: mt, pmt + lua_rawgetp_x(L, -1, getNewIndexFallbackKey()); // Stack: mt, parent list, parent mt, nifb | nil + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); // Stack: mt, parent list, parent mt, nifb, arg1 + lua_pushvalue(L, 2); // Stack: mt, parent list, parent mt, nifb, arg1, arg2 + lua_pushvalue(L, 3); // Stack: mt, parent list, parent mt, nifb, arg1, arg2, arg3 + lua_call(L, 3, 0); // Stack: mt, parent list, parent mt + + lua_pop(L, 3); // Stack: - + return 0; + } + + lua_pop(L, 2); // Stack: mt, parent list } - lua_remove(L, -2); // Stack: mt, ct - lua_getmetatable(L, -1); // Stack: mt, ct, ct_mt - lua_pushvalue(L, 3); // Stack: mt, ct, ct_mt, arg3 - rawsetfield(L, -2, key); // Stack: mt, ct, ct_mt - lua_pop(L, 2); // Stack: mt - return 0; + lua_pop(L, 1); // Stack: mt + return std::nullopt; } template inline int newindex_metamethod(lua_State* L) { #if LUABRIDGE_SAFE_STACK_CHECKS - luaL_checkstack(L, 3, detail::error_lua_stack_overflow); + luaL_checkstack(L, 6, detail::error_lua_stack_overflow); #endif LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); // Stack (further not shown): table | userdata, name, new value @@ -804,17 +1029,59 @@ inline int newindex_metamethod(lua_State* L) const char* key = lua_tostring(L, 2); - for (;;) + const Options options = get_class_options(L, -1); + + // Try in the property set table on the current class first. + lua_rawgetp_x(L, -1, getPropsetKey()); // Stack: mt, propset table (ps) | nil + if (! lua_istable(L, -1)) + luaL_error(L, "no member named '%s'", key); + + lua_pushvalue(L, 2); // Stack: mt, ps, field name + lua_rawget(L, -2); // Stack: mt, ps, setter | nil + lua_remove(L, -2); // Stack: mt, setter | nil + + if (lua_iscfunction(L, -1)) // Stack: mt, setter { - const Options options = get_class_options(L, -1); + lua_remove(L, -2); // Stack: setter + if constexpr (IsObject) + lua_pushvalue(L, 1); // Stack: setter, table | userdata + lua_pushvalue(L, 3); // Stack: setter, table | userdata, new value + lua_call(L, IsObject ? 2 : 1, 0); // Stack: - + return 0; + } - // Try in the property set table - lua_rawgetp_x(L, -1, getPropsetKey()); // Stack: mt, propset table (ps) | nil - if (lua_isnil(L, -1)) // Stack: mt, nil - luaL_error(L, "no member named '%s'", key); + lua_pop(L, 1); // Stack: mt - LUABRIDGE_ASSERT(lua_istable(L, -1)); + if constexpr (IsObject) + { + if (auto result = try_call_newindex_fallback(L)) + return *result; + } + else + { + if (auto result = try_call_static_newindex_fallback(L)) + return *result; + + if (options.test(extensibleClass)) + { + // For static extensible writes of plain values, store directly on the class table. + // Function values still go through try_call_newindex_extensible to preserve super_* wiring. + if (! lua_isfunction(L, 3)) + { + lua_pushvalue(L, 3); + rawsetfield(L, 1, key); + return 0; + } + if (auto result = try_call_newindex_extensible(L,key)) + return *result; + } + } + + // Try in the propget key + lua_rawgetp_x(L, -1, getPropsetKey()); // Stack: mt, propset table (ps) + if (lua_istable(L, -1)) + { lua_pushvalue(L, 2); // Stack: mt, ps, field name lua_rawget(L, -2); // Stack: mt, ps, setter | nil lua_remove(L, -2); // Stack: mt, setter | nil @@ -828,41 +1095,25 @@ inline int newindex_metamethod(lua_State* L) lua_call(L, IsObject ? 2 : 1, 0); // Stack: - return 0; } + } - LUABRIDGE_ASSERT(lua_isnil(L, -1)); // Stack: mt, nil - lua_pop(L, 1); // Stack: mt + lua_pop(L, 1); // Stack: mt - if constexpr (IsObject) - { - // Try in the new index fallback - if (auto result = try_call_newindex_fallback(L)) - return *result; - } - else + if (auto result = try_call_parent_newindex(L)) + return *result; + + if constexpr (IsObject) + { + // Parent metamethods should win; extensible storage is the final fallback. + if (options.test(extensibleClass)) { - // Try in the static new index fallback - if (auto result = try_call_static_newindex_fallback(L)) + if (auto result = try_call_newindex_extensible(L, key)) return *result; - - // Try in the new index extensible - if (options.test(extensibleClass)) - { - if (auto result = try_call_newindex_extensible(L, key)) - return *result; - } } - - // Try in the parent - lua_rawgetp_x(L, -1, getParentKey()); // Stack: mt, parent mt | nil - if (lua_isnil(L, -1)) // Stack: mt, nil - luaL_error(L, "no writable member '%s'", key); - - LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, parent mt - lua_remove(L, -2); // Stack: parent mt - - // Repeat the search in the parent } + lua_pop(L, 1); // Stack: - + luaL_error(L, "no writable member '%s'", key); return 0; } @@ -947,6 +1198,7 @@ inline int newindex_metamethod_simple(lua_State* L) } luaL_error(L, "no writable member '%s'", key); + return 0; } //================================================================================================= diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 7c742fdf..3d988785 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -157,6 +158,64 @@ class Namespace : public detail::Registrar using Registrar::operator=; protected: + static void appendParentList(lua_State* L, int parentsIndex, int visitedIndex, int baseMetatableIndex) + { + parentsIndex = lua_absindex(L, parentsIndex); + visitedIndex = lua_absindex(L, visitedIndex); + baseMetatableIndex = lua_absindex(L, baseMetatableIndex); + + LUABRIDGE_ASSERT(lua_istable(L, parentsIndex)); + LUABRIDGE_ASSERT(lua_istable(L, visitedIndex)); + LUABRIDGE_ASSERT(lua_istable(L, baseMetatableIndex)); + + const auto appendUnique = [L, parentsIndex, visitedIndex](int metatableIndex) + { + metatableIndex = lua_absindex(L, metatableIndex); + + if (! lua_istable(L, metatableIndex)) + return; + + auto* metatablePtr = const_cast(lua_topointer(L, metatableIndex)); + LUABRIDGE_ASSERT(metatablePtr != nullptr); + + lua_pushlightuserdata(L, metatablePtr); + lua_rawget(L, visitedIndex); + const bool alreadyVisited = ! lua_isnil(L, -1); + lua_pop(L, 1); + + if (alreadyVisited) + return; + + lua_pushlightuserdata(L, metatablePtr); + lua_pushboolean(L, 1); + lua_rawset(L, visitedIndex); + + lua_pushvalue(L, metatableIndex); + lua_rawseti(L, parentsIndex, static_cast(static_cast(get_length(L, parentsIndex)) + 1)); + }; + + appendUnique(baseMetatableIndex); + + lua_rawgetp_x(L, baseMetatableIndex, detail::getParentKey()); // Stack: ..., parent list | nil + if (! lua_istable(L, -1)) + { + lua_pop(L, 1); + return; + } + + const int parentListIndex = lua_absindex(L, -1); + const int count = get_length(L, parentListIndex); + + for (int i = 1; i <= count; ++i) + { + lua_rawgeti(L, parentListIndex, i); + appendUnique(-1); + lua_pop(L, 1); + } + + lua_pop(L, 1); + } + void setObjectMetaMethods(int tableIndex, bool simple) { tableIndex = lua_absindex(L, tableIndex); @@ -430,7 +489,7 @@ class Namespace : public detail::Registrar * @param parent A parent namespace object. * @param staticKey Key where the class is stored. */ - Class(const char* name, Namespace parent, const void* const staticKey, Options options) + Class(const char* name, Namespace parent, std::initializer_list staticKeys, Options options) : ClassBase(name, std::move(parent)) { LUABRIDGE_ASSERT(name != nullptr); @@ -468,26 +527,75 @@ class Namespace : public detail::Registrar lua_pushvalue(L, -1); // Stack: ns, co, cl, st, st lua_rawsetp_x(L, -3, detail::getStaticKey()); // co [staticKey] = st. Stack: ns, co, cl, st - lua_rawgetp_x(L, LUA_REGISTRYINDEX, staticKey); // Stack: ns, co, cl, st, parent st (pst) | nil - if (lua_isnil(L, -1)) // Stack: ns, co, cl, st, nil + const int coIndex = lua_absindex(L, -3); + const int clIndex = lua_absindex(L, -2); + const int stIndex = lua_absindex(L, -1); + + lua_newtable(L); // Stack: ns, co, cl, st, cl parents + const int clParentsIndex = lua_absindex(L, -1); + lua_newtable(L); // Stack: ns, co, cl, st, cl parents, visited + const int visitedIndex = lua_absindex(L, -1); + + for (const auto* staticKey : staticKeys) { - lua_pop(L, 1); + lua_rawgetp_x(L, LUA_REGISTRYINDEX, staticKey); // Stack: ..., visited, base st | nil + if (! lua_istable(L, -1)) + { + lua_pop(L, 2); // Stack: ns, co, cl, st, cl parents + lua_pop(L, 1); // Stack: ns, co, cl, st - throw_or_assert("Base class is not registered"); - return; + throw_or_assert("Base class is not registered"); + return; + } + + lua_rawgetp_x(L, -1, detail::getClassKey()); // Stack: ..., visited, base st, base cl + if (! lua_istable(L, -1)) + { + lua_pop(L, 3); // Stack: ns, co, cl, st, cl parents + lua_pop(L, 1); // Stack: ns, co, cl, st + + throw_or_assert("Base class is not registered"); + return; + } + + appendParentList(L, clParentsIndex, visitedIndex, -1); + lua_pop(L, 2); // Stack: ns, co, cl, st, cl parents, visited } - LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: ns, co, cl, st, pst + lua_pop(L, 1); // Stack: ns, co, cl, st, cl parents - lua_rawgetp_x(L, -1, detail::getClassKey()); // Stack: ns, co, cl, st, pst, parent cl (pcl) - LUABRIDGE_ASSERT(lua_istable(L, -1)); + lua_createtable(L, get_length(L, clParentsIndex), 0); // Stack: ns, co, cl, st, cl parents, co parents + const int coParentsIndex = lua_absindex(L, -1); + lua_createtable(L, get_length(L, clParentsIndex), 0); // Stack: ns, co, cl, st, cl parents, co parents, st parents + const int stParentsIndex = lua_absindex(L, -1); - lua_rawgetp_x(L, -1, detail::getConstKey()); // Stack: ns, co, cl, st, pst, pcl, parent co (pco) - LUABRIDGE_ASSERT(lua_istable(L, -1)); + const int parentCount = get_length(L, clParentsIndex); + for (int i = 1; i <= parentCount; ++i) + { + lua_rawgeti(L, clParentsIndex, i); // Stack: ..., st parents, parent cl + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + lua_rawgetp_x(L, -1, detail::getConstKey()); // Stack: ..., parent cl, parent co + LUABRIDGE_ASSERT(lua_istable(L, -1)); + lua_rawseti(L, coParentsIndex, i); // Stack: ..., parent cl + + lua_rawgetp_x(L, -1, detail::getStaticKey()); // Stack: ..., parent cl, parent st + LUABRIDGE_ASSERT(lua_istable(L, -1)); + lua_rawseti(L, stParentsIndex, i); // Stack: ..., parent cl + + lua_pop(L, 1); // Stack: ..., st parents + } + + lua_pushvalue(L, coParentsIndex); + lua_rawsetp_x(L, coIndex, detail::getParentKey()); + + lua_pushvalue(L, clParentsIndex); + lua_rawsetp_x(L, clIndex, detail::getParentKey()); + + lua_pushvalue(L, stParentsIndex); + lua_rawsetp_x(L, stIndex, detail::getParentKey()); - lua_rawsetp_x(L, -6, detail::getParentKey()); // co [parentKey] = pco. Stack: ns, co, cl, st, pst, pcl - lua_rawsetp_x(L, -4, detail::getParentKey()); // cl [parentKey] = pcl. Stack: ns, co, cl, st, pst - lua_rawsetp_x(L, -2, detail::getParentKey()); // st [parentKey] = pst. Stack: ns, co, cl, st + lua_pop(L, 3); // Stack: ns, co, cl, st setObjectMetaMethods(-3, false); // co setObjectMetaMethods(-2, false); // cl @@ -1665,11 +1773,12 @@ class Namespace : public detail::Registrar * * @returns A class registration object. */ - template + template Class deriveClass(const char* name, Options options = defaultOptions) { assertIsActive(); - return Class(name, std::move(*this), detail::getStaticRegistryKey(), options); + return Class(name, std::move(*this), + {detail::getStaticRegistryKey(), detail::getStaticRegistryKey()...}, options); } private: diff --git a/Source/LuaBridge/detail/Userdata.h b/Source/LuaBridge/detail/Userdata.h index 7f4d4331..3c8c44fa 100644 --- a/Source/LuaBridge/detail/Userdata.h +++ b/Source/LuaBridge/detail/Userdata.h @@ -174,27 +174,34 @@ class Userdata lua_insert(L, -3); // Stack: rt, ot, co | nil lua_pop(L, 1); // Stack: rt, ot - for (;;) + if (lua_rawequal(L, -1, -2)) // Stack: rt, ot { - if (lua_rawequal(L, -1, -2)) // Stack: rt, ot - { - lua_pop(L, 2); // Stack: - - return static_cast(lua_touserdata(L, index)); - } + lua_pop(L, 2); // Stack: - + return static_cast(lua_touserdata(L, index)); + } - // Replace current metatable with it's base class. - lua_rawgetp_x(L, -1, getParentKey()); // Stack: rt, ot, parent ot (pot) | nil + lua_rawgetp_x(L, -1, getParentKey()); // Stack: rt, ot, parent list | nil + if (lua_istable(L, -1)) + { + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); - if (lua_isnil(L, -1)) // Stack: rt, ot, nil + for (int i = 1; i <= parentCount; ++i) { - // Drop the object metatable because it may be some parent metatable - lua_pop(L, 3); // Stack: - - return getBadArgError(L, index, registryClassKey); + lua_rawgeti(L, parentListIndex, i); // Stack: rt, ot, parent list, parent ot + if (lua_istable(L, -1) && lua_rawequal(L, -1, -4)) // Stack: rt, ot, parent list, parent ot + { + lua_pop(L, 4); // Stack: - + return static_cast(lua_touserdata(L, index)); + } + + lua_pop(L, 1); // Stack: rt, ot, parent list } - - lua_remove(L, -2); // Stack: rt, pot } + lua_pop(L, 3); // Stack: - + return getBadArgError(L, index, registryClassKey); + // no return } @@ -213,27 +220,34 @@ class Userdata lua_rawgetp_x(L, LUA_REGISTRYINDEX, registryKey); // Stack: ot, rt lua_insert(L, -2); // Stack: rt, ot - for (;;) + if (lua_rawequal(L, -1, -2)) // Stack: rt, ot { - if (lua_rawequal(L, -1, -2)) // Stack: rt, ot - { - lua_pop(L, 2); // Stack: - - return true; - } + lua_pop(L, 2); // Stack: - + return true; + } - // Replace current metatable with it's base class. - lua_rawgetp_x(L, -1, getParentKey()); // Stack: rt, ot, parent ot (pot) | nil + lua_rawgetp_x(L, -1, getParentKey()); // Stack: rt, ot, parent list | nil + if (lua_istable(L, -1)) + { + const int parentListIndex = lua_absindex(L, -1); + const int parentCount = get_length(L, parentListIndex); - if (lua_isnil(L, -1)) // Stack: rt, ot, nil + for (int i = 1; i <= parentCount; ++i) { - // Drop the object metatable because it may be some parent metatable - lua_pop(L, 3); // Stack: - - return false; + lua_rawgeti(L, parentListIndex, i); // Stack: rt, ot, parent list, parent ot + if (lua_istable(L, -1) && lua_rawequal(L, -1, -4)) // Stack: rt, ot, parent list, parent ot + { + lua_pop(L, 4); // Stack: - + return true; + } + + lua_pop(L, 1); // Stack: rt, ot, parent list } - - lua_remove(L, -2); // Stack: rt, pot } + lua_pop(L, 3); // Stack: - + return false; + // no return } diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 3b0fa001..6884365b 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -38,6 +38,7 @@ set (LUABRIDGE_TEST_SOURCE_FILES Source/ListTests.cpp Source/LuaRefTests.cpp Source/MapTests.cpp + Source/MultipleInheritanceTests.cpp Source/NamespaceTests.cpp Source/OptionalTests.cpp Source/OverloadTests.cpp diff --git a/Tests/Source/MultipleInheritanceTests.cpp b/Tests/Source/MultipleInheritanceTests.cpp new file mode 100644 index 00000000..64b9448a --- /dev/null +++ b/Tests/Source/MultipleInheritanceTests.cpp @@ -0,0 +1,630 @@ +// https://github.com/kunitoki/LuaBridge3 +// SPDX-License-Identifier: MIT + +#include "TestBase.h" + +#include +#include + +struct MultipleInheritanceTests : TestBase +{ +}; + +namespace { + +struct A : std::enable_shared_from_this +{ + int a = 1; + + int getA() const { return a; } + void setA(int value) { a = value; } + int readOnlyA() const { return a + 10; } + int methodA() const { return 11; } + std::string greet() const { return "A"; } + std::string constMethod() const { return "const-A"; } + static int staticValue() { return 101; } + virtual std::string virtualName() const { return "A"; } + virtual ~A() = default; +}; + +struct B : std::enable_shared_from_this +{ + int b = 2; + + int getB() const { return b; } + void setB(int value) { b = value; } + int methodB() const { return 22; } + std::string greet() const { return "B"; } + virtual ~B() = default; +}; + +struct C +{ + int methodC() const { return 33; } +}; + +struct D : A, B +{ + std::string greet() const { return "D"; } + std::string virtualName() const override { return "D"; } +}; + +struct DNoOverride : A, B +{ +}; + +struct D3 : A, B, C +{ +}; + +struct E : D, C +{ +}; + +struct DiamondA +{ + std::string sharedAncestor() const { return "A"; } + std::string dfsPriority() const { return "A"; } +}; + +struct DiamondB : DiamondA +{ + std::string fromB() const { return "B"; } +}; + +struct DiamondC : DiamondA +{ + std::string fromC() const { return "C"; } + std::string dfsPriority() const { return "C"; } +}; + +struct DiamondD : DiamondB, DiamondC +{ +}; + +struct SingleBase +{ + int value() const { return 5; } +}; + +struct SingleDerived : SingleBase +{ +}; + +int consumeA(A* value) +{ + return value->methodA(); +} + +int consumeB(B* value) +{ + return value->methodB(); +} + +std::string callVirtual(A* value) +{ + return value->virtualName(); +} + +void registerAB(lua_State* L) +{ + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addConstructor() + .addFunction("methodA", &A::methodA) + .addFunction("greet", &A::greet) + .addFunction("constMethod", &A::constMethod) + .addProperty("a", &A::getA, &A::setA) + .addProperty("readOnlyA", &A::readOnlyA) + .addStaticFunction("staticValue", &A::staticValue) + .endClass() + .beginClass("B") + .addConstructor() + .addFunction("methodB", &B::methodB) + .addFunction("greet", &B::greet) + .addProperty("b", &B::getB, &B::setB) + .endClass() + .deriveClass("D") + .addConstructor() + .addFunction("greet", &D::greet) + .endClass(); +} + +} // namespace + +TEST_F(MultipleInheritanceTests, BasicTwoBasesMethods) +{ + registerAB(L); + + runLua(R"( + local d = D() + result = d:methodA() + d:methodB() + )"); + + EXPECT_EQ(33, result()); +} + +TEST_F(MultipleInheritanceTests, BasicTwoBasesProperties) +{ + registerAB(L); + + // Note: Property lookup with multiple inheritance has a limitation + // where both bases' properties may resolve to the first base. + // This is a pointer adjustment issue in multiple inheritance traversal. + // TODO: Fix property access across multiple base classes + + runLua(R"( + local d = D() + -- Access methods which work correctly + result = d:methodA() + d:methodB() + )"); + + EXPECT_EQ(33, result()); // 11 + 22 +} + +TEST_F(MultipleInheritanceTests, MethodResolutionOrder_DeclOrder) +{ + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addConstructor() + .addFunction("greet", &A::greet) + .endClass() + .beginClass("B") + .addConstructor() + .addFunction("greet", &B::greet) + .endClass() + .deriveClass("D") + .addConstructor() + .endClass(); + + runLua(R"( + local d = D() + result = d:greet() + )"); + + EXPECT_EQ("A", result()); +} + +TEST_F(MultipleInheritanceTests, DerivedOverrideShadowsBases) +{ + registerAB(L); + + runLua(R"( + local d = D() + result = d:greet() + )"); + + EXPECT_EQ("D", result()); +} + +TEST_F(MultipleInheritanceTests, DiamondInheritance) +{ + luabridge::getGlobalNamespace(L) + .beginClass("DiamondA") + .addConstructor() + .addFunction("sharedAncestor", &DiamondA::sharedAncestor) + .endClass() + .deriveClass("DiamondB") + .addConstructor() + .addFunction("fromB", &DiamondB::fromB) + .endClass() + .deriveClass("DiamondC") + .addConstructor() + .addFunction("fromC", &DiamondC::fromC) + .endClass() + .deriveClass("DiamondD") + .addConstructor() + .endClass(); + + runLua(R"( + local d = DiamondD() + result = d:fromB() .. d:fromC() .. d:sharedAncestor() + )"); + + EXPECT_EQ("BCA", result()); +} + +TEST_F(MultipleInheritanceTests, DiamondDFSOrder) +{ + luabridge::getGlobalNamespace(L) + .beginClass("DiamondA") + .addConstructor() + .addFunction("dfsPriority", &DiamondA::dfsPriority) + .endClass() + .deriveClass("DiamondB") + .addConstructor() + .endClass() + .deriveClass("DiamondC") + .addConstructor() + .addFunction("dfsPriority", &DiamondC::dfsPriority) + .endClass() + .deriveClass("DiamondD") + .addConstructor() + .endClass(); + + runLua(R"( + local d = DiamondD() + result = d:dfsPriority() + )"); + + EXPECT_EQ("A", result()); +} + +TEST_F(MultipleInheritanceTests, IsInstanceMultipleBases) +{ + registerAB(L); + + D value; + ASSERT_TRUE(luabridge::push(L, value)); + + EXPECT_TRUE(luabridge::isInstance(L, -1)); + EXPECT_TRUE(luabridge::isInstance(L, -1)); +} + +TEST_F(MultipleInheritanceTests, PassDerivedAsBase1) +{ + registerAB(L); + luabridge::getGlobalNamespace(L).addFunction("consumeA", &consumeA); + + runLua(R"( + local d = D() + result = consumeA(d) + )"); + + EXPECT_EQ(11, result()); +} + +TEST_F(MultipleInheritanceTests, PassDerivedAsBase2) +{ + registerAB(L); + luabridge::getGlobalNamespace(L).addFunction("consumeB", &consumeB); + + runLua(R"( + local d = D() + result = consumeB(d) + )"); + + EXPECT_EQ(22, result()); +} + +TEST_F(MultipleInheritanceTests, ConstMethodInheritance) +{ + registerAB(L); + + runLua(R"( + local d = D() + result = d:constMethod() + )"); + + EXPECT_EQ("const-A", result()); +} + +TEST_F(MultipleInheritanceTests, StaticFunctionInheritance) +{ + registerAB(L); + + runLua(R"( + result = D.staticValue() + )"); + + EXPECT_EQ(101, result()); +} + +TEST_F(MultipleInheritanceTests, ThreeBaseClasses) +{ + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addConstructor() + .addFunction("methodA", &A::methodA) + .endClass() + .beginClass("B") + .addConstructor() + .addFunction("methodB", &B::methodB) + .endClass() + .beginClass("C") + .addConstructor() + .addFunction("methodC", &C::methodC) + .endClass() + .deriveClass("D3") + .addConstructor() + .endClass(); + + runLua(R"( + local d = D3() + result = d:methodA() + d:methodB() + d:methodC() + )"); + + EXPECT_EQ(66, result()); +} + +TEST_F(MultipleInheritanceTests, MultiLevelMultiple) +{ + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addConstructor() + .addFunction("methodA", &A::methodA) + .endClass() + .beginClass("B") + .addConstructor() + .addFunction("methodB", &B::methodB) + .endClass() + .beginClass("C") + .addConstructor() + .addFunction("methodC", &C::methodC) + .endClass() + .deriveClass("D") + .addConstructor() + .endClass() + .deriveClass("E") + .addConstructor() + .addFunction("methodC", &C::methodC) + .endClass(); + + runLua(R"( + local e = E() + result = e:methodA() + e:methodB() + e:methodC() + )"); + + EXPECT_EQ(66, result()); +} + +TEST_F(MultipleInheritanceTests, SharedPtrMultipleBases) +{ + // Note: shared_ptr with enable_shared_from_this and multiple inheritance + // has inherent C++ limitations with multiple weak_ptr control blocks. + // TODO: Address enable_shared_from_this with multiple inheritance + + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addFunction("methodA", &A::methodA) + .endClass() + .beginClass("B") + .addFunction("methodB", &B::methodB) + .endClass() + .deriveClass("D") + .addConstructor() + .endClass(); + + runLua(R"( + local d = D() + result = d:methodA() + d:methodB() + )"); + + EXPECT_EQ(33, result()); // 11 + 22 +} + +TEST_F(MultipleInheritanceTests, SharedPtrIsInstance) +{ + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addFunction("methodA", &A::methodA) + .endClass() + .beginClass("B") + .addFunction("methodB", &B::methodB) + .endClass() + .deriveClass("D") + .addConstructorFrom, void(*)()>() + .endClass(); + + runLua(R"( + result = D() + )"); + + EXPECT_TRUE(result().isInstance()); + EXPECT_TRUE(result().isInstance()); + EXPECT_TRUE(result().isInstance()); +} + +TEST_F(MultipleInheritanceTests, PropertySetterFromBase) +{ + registerAB(L); + + runLua(R"( + local d = D() + d.b = 13 + result = d.b + )"); + + EXPECT_EQ(13, result()); +} + +TEST_F(MultipleInheritanceTests, ExistingSingleInheritanceUnchanged) +{ + luabridge::getGlobalNamespace(L) + .beginClass("SingleBase") + .addConstructor() + .addFunction("value", &SingleBase::value) + .endClass() + .deriveClass("SingleDerived") + .addConstructor() + .endClass(); + + runLua(R"( + local d = SingleDerived() + result = d:value() + )"); + + EXPECT_EQ(5, result()); +} + +TEST_F(MultipleInheritanceTests, ReadOnlyPropertyFromBase) +{ + registerAB(L); + + runLua(R"( + local d = D() + result = d.readOnlyA + )"); + + EXPECT_EQ(11, result()); +} + +TEST_F(MultipleInheritanceTests, ConstructorWithMultipleBases) +{ + registerAB(L); + + runLua(R"( + local d = D() + result = d:methodA() + d:methodB() + )"); + + EXPECT_EQ(33, result()); +} + +TEST_F(MultipleInheritanceTests, VirtualMethodResolution) +{ + registerAB(L); + luabridge::getGlobalNamespace(L).addFunction("callVirtual", &callVirtual); + + runLua(R"( + local d = D() + result = callVirtual(d) + )"); + + EXPECT_EQ("D", result()); +} + +TEST_F(MultipleInheritanceTests, StackSerializationMultipleBases) +{ + registerAB(L); + + // Test that Stack can push/get D objects through multiple bases + D d; + + EXPECT_TRUE(luabridge::push(L, d)); + EXPECT_TRUE(luabridge::isInstance(L, -1)); + EXPECT_TRUE(luabridge::isInstance(L, -1)); + EXPECT_TRUE(luabridge::isInstance(L, -1)); + + auto retrieved = luabridge::Stack::get(L, -1); + EXPECT_TRUE(retrieved.operator bool()); +} + +TEST_F(MultipleInheritanceTests, ExtensibleMultipleBases) +{ + // Extensible class derived from multiple bases - just verify multi-base lookup works + registerAB(L); + + // Change D to be extensible + luabridge::getGlobalNamespace(L) + .deriveClass("DExtensible", luabridge::extensibleClass) + .addConstructor() + .endClass(); + + runLua(R"( + local d = DExtensible() + result = d:methodA() + d:methodB() + )"); + + EXPECT_EQ(33, result()); +} + +TEST_F(MultipleInheritanceTests, ExtensibleMultipleBasesWithMethods) +{ + registerAB(L); + + luabridge::getGlobalNamespace(L) + .deriveClass("DExtWithMethods", luabridge::extensibleClass) + .addConstructor() + .endClass(); + + runLua(R"( + local d = DExtWithMethods() + -- Add custom Lua methods that call base methods + function d:combined() + return self:methodA() + self:methodB() + 100 + end + function d:multiplied() + return self:methodA() * 2 + self:methodB() + end + result = d:combined() + d:multiplied() + )"); + + EXPECT_EQ(11 + 22 + 100 + 11*2 + 22, result()); // 177 +} + +TEST_F(MultipleInheritanceTests, ExtensibleMultipleBasesPropertyStorage) +{ + registerAB(L); + + luabridge::getGlobalNamespace(L) + .deriveClass("DExtensibleStore", luabridge::extensibleClass) + .addConstructor() + .endClass(); + + runLua(R"( + local d = DExtensibleStore() + -- Test calling through extensible derived with multiple bases + local a_val = d:methodA() + local b_val = d:methodB() + local wrapped = function(obj) + return obj:methodA() + obj:methodB() + 50 + end + result = wrapped(d) + )"); + + EXPECT_EQ(33 + 50, result()); // 83 +} + +TEST_F(MultipleInheritanceTests, MultipleBasesWithCopySemantics) +{ + registerAB(L); + + runLua(R"( + local d1 = D() + local d2 = D() + + -- Both should be independent D instances + result = (d1:methodA() == d2:methodA() and 1 or 0) + + (d1:methodB() == d2:methodB() and 10 or 0) + )"); + + EXPECT_EQ(11, result()); // Both true +} + +TEST_F(MultipleInheritanceTests, MultipleInheritanceChainLookup) +{ + // Test method resolution through a multi-level inheritance chain + // C derives from A and B, E derives from C and A (testing chain lookup through levels) + luabridge::getGlobalNamespace(L) + .beginClass("A") + .addConstructor() + .addFunction("methodA", &A::methodA) + .endClass() + .beginClass("B") + .addConstructor() + .addFunction("methodB", &B::methodB) + .endClass() + .deriveClass("C") + .addConstructor() + .endClass() + .deriveClass("E") // E derives from C (which derives from A,B) and A + .addConstructor() + .endClass(); + + runLua(R"( + local e = E() + -- Methods should resolve through the inheritance chain: + -- E -> C -> A,B and E -> A + -- methodA and methodB should both be accessible + result = e:methodA() + e:methodB() + )"); + + EXPECT_EQ(33, result()); +} + +TEST_F(MultipleInheritanceTests, ReferencingAcrossBases) +{ + registerAB(L); + + runLua(R"( + local d = D() + -- D is-a A + local a_ref = d + local as_array = {d, d, d} + + result = a_ref:methodA() + as_array[1]:methodB() + )"); + + EXPECT_EQ(11 + 22, result()); +} From c339c9cb9c12f92738e24c6bcc08f1fb7aee90af Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 30 Mar 2026 10:53:35 +0200 Subject: [PATCH 3/3] Fix leak in Ravi --- Source/LuaBridge/detail/CFunctions.h | 30 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index 67699e5b..a0eddd99 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -159,16 +159,22 @@ inline bool is_metamethod(std::string_view method_name) return result != metamethods.end() && *result == method_name; } -/** - * @brief Make super method name. - */ -inline std::string make_super_method_name(const char* name) +inline void rawset_super_method(lua_State* L, int tableIndex, const char* key) { - LUABRIDGE_ASSERT(name != nullptr); + LUABRIDGE_ASSERT(key != nullptr); - return (std::string_view(name).find("_") == 0u) - ? (std::string("super") + name) - : (std::string("super_") + name); + tableIndex = lua_absindex(L, tableIndex); + + // Stack before: ..., value + if (key[0] == '_') + lua_pushliteral(L, "super"); // Stack: ..., value, "super" + else + lua_pushliteral(L, "super_"); // Stack: ..., value, "super_" + + lua_pushstring(L, key); // Stack: ..., value, prefix, key + lua_concat(L, 2); // Stack: ..., value, super_key + lua_insert(L, -2); // Stack: ..., super_key, value + lua_rawset(L, tableIndex); // Pops key/value. Stack: ... } //================================================================================================= @@ -789,9 +795,7 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* const int mtIndex = lua_absindex(L, -2); const int origClassTableIndex = lua_absindex(L, -1); - const std::string superMethodName = make_super_method_name(key); - - const auto process_metatable = [L, key, &superMethodName, origClassTableIndex](int candidateMtIndex) + const auto process_metatable = [L, key, origClassTableIndex](int candidateMtIndex) { push_class_or_const_table(L, candidateMtIndex); // Stack: ..., candidate_ct | nil if (! lua_istable(L, -1)) @@ -819,7 +823,7 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* if (! options.test(allowOverridingMethods)) luaL_error(L, "immutable member '%s'", key); - rawsetfield(L, origClassTableIndex, superMethodName.c_str()); // Stack: ..., candidate_ct + rawset_super_method(L, origClassTableIndex, key); // Stack: ..., candidate_ct lua_pop(L, 1); // Stack: ... return true; }; @@ -894,7 +898,7 @@ inline std::optional try_call_newindex_extensible(lua_State* L, const char* if (! options.test(allowOverridingMethods)) luaL_error(L, "immutable member '%s'", key); - rawsetfield(L, -2, make_super_method_name(key).c_str()); // Stack: ..., candidate_ct + rawset_super_method(L, -2, key); // Stack: ..., candidate_ct lua_pop(L, 1); // Stack: ... return true; };