Skip to content

Commit c221b10

Browse files
authored
Fix extensible class bugs (#209)
1 parent 3254c8f commit c221b10

5 files changed

Lines changed: 183 additions & 20 deletions

File tree

.codeql.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
paths:
2+
- 'Source'

.github/workflows/amalgamate.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ jobs:
1818
uses: actions/checkout@v6
1919

2020
- name: Set up Python
21-
uses: actions/setup-python@v5
21+
uses: actions/setup-python@v6
2222
with:
2323
python-version: 3.9
2424

2525
- name: Create amalgamation file
2626
run: python amalgamate.py
2727

2828
- name: Commit changes
29-
uses: EndBug/add-and-commit@v9
29+
uses: EndBug/add-and-commit@v10
3030
with:
3131
committer_name: GitHub Actions
3232
committer_email: actions@github.com

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
- "**/ThirdParty/**"
1111
- "**/CMakeLists.txt"
1212
- "**/.gitmodules"
13+
- "**/.codeql.yml"
1314
- "!**/*.md"
1415
- "!**/*.txt"
1516

@@ -45,6 +46,7 @@ jobs:
4546
uses: github/codeql-action/init@v3
4647
with:
4748
languages: ${{ matrix.language }}
49+
config-file: ./.codeql.yml
4850

4951
- name: Create Build Environment
5052
run: cmake -E make_directory ${{runner.workspace}}/build

Source/LuaBridge/detail/CFunctions.h

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -429,58 +429,134 @@ inline std::optional<int> try_call_newindex_extensible(lua_State* L, const char*
429429
LUABRIDGE_ASSERT(key != nullptr);
430430
LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt
431431

432+
// For Lua function values (instance methods added from Lua), capture the originating class
433+
// table so the method is stored there rather than in a traversed-to parent class table.
434+
// This prevents e.g. `function Derived:init()` from polluting `Base`'s method table.
435+
//
436+
// For non-function values (static properties like `Class.prop = val`), the original
437+
// traversal-end behaviour is preserved: storing them in the nearest class table that
438+
// already has a matching key (or the topmost base if none exists). This keeps static
439+
// properties out of the derived class table, which is also used for instance extensible
440+
// method lookup, avoiding unintended shadowing of per-instance properties accessed via an
441+
// index-fallback metamethod registered on a non-extensible base class.
442+
const bool value_is_function = lua_isfunction(L, 3);
443+
444+
if (value_is_function)
445+
{
446+
// Capture the original (most-derived) class table — always write here.
447+
push_class_or_const_table(L, -1); // Stack: mt, orig_ct | nil
448+
if (! lua_istable(L, -1)) // Stack: mt, nil
449+
{
450+
lua_pop(L, 1); // Stack: mt
451+
return std::nullopt;
452+
}
453+
// Stack: mt, orig_ct
454+
455+
lua_pushvalue(L, -2); // Stack: mt, orig_ct, cur_mt (traversal copy)
456+
457+
for (;;)
458+
{
459+
push_class_or_const_table(L, -1); // Stack: mt, orig_ct, cur_mt, cur_ct | nil
460+
if (! lua_istable(L, -1)) // Stack: mt, orig_ct, cur_mt, nil
461+
{
462+
lua_pop(L, 2); // Stack: mt, orig_ct
463+
break;
464+
}
465+
466+
lua_pushvalue(L, 2); // Stack: mt, orig_ct, cur_mt, cur_ct, field name
467+
lua_rawget(L, -2); // Stack: mt, orig_ct, cur_mt, cur_ct, field | nil
468+
469+
if (! lua_isnil(L, -1)) // Stack: mt, orig_ct, cur_mt, cur_ct, field
470+
{
471+
if (! lua_iscfunction(L, -1))
472+
{
473+
lua_pop(L, 3); // Stack: mt, orig_ct
474+
break;
475+
}
476+
477+
const Options options = get_class_options(L, -2); // Stack: mt, orig_ct, cur_mt, cur_ct, field
478+
if (! options.test(allowOverridingMethods))
479+
luaL_error(L, "immutable member '%s'", key);
480+
481+
// Store super_ alias in the ORIGINAL (derived) class table so only derived
482+
// instances can call it; the base class table is left completely untouched.
483+
rawsetfield(L, -4, make_super_method_name(key).c_str()); // Stack: mt, orig_ct, cur_mt, cur_ct
484+
lua_pop(L, 2); // Stack: mt, orig_ct
485+
break;
486+
}
487+
488+
lua_pop(L, 1); // Stack: mt, orig_ct, cur_mt, cur_ct
489+
490+
lua_rawgetp_x(L, -2, getParentKey()); // Stack: mt, orig_ct, cur_mt, cur_ct, pmt | nil
491+
if (lua_isnil(L, -1)) // Stack: mt, orig_ct, cur_mt, cur_ct, nil
492+
{
493+
lua_pop(L, 3); // Stack: mt, orig_ct
494+
break;
495+
}
496+
497+
LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, orig_ct, cur_mt, cur_ct, pmt
498+
lua_remove(L, -2); // Stack: mt, orig_ct, cur_mt, pmt
499+
lua_remove(L, -2); // Stack: mt, orig_ct, pmt
500+
}
501+
502+
// Stack: mt, orig_ct — write to the original (most-derived) class table.
503+
lua_getmetatable(L, -1); // Stack: mt, orig_ct, orig_ct_mt
504+
lua_pushvalue(L, 3); // Stack: mt, orig_ct, orig_ct_mt, arg3
505+
rawsetfield(L, -2, key); // Stack: mt, orig_ct, orig_ct_mt
506+
lua_pop(L, 2); // Stack: mt
507+
return 0;
508+
}
509+
510+
// Non-function value (static property): use original traversal-end write location.
432511
lua_pushvalue(L, -1); // Stack: mt, mt
433512

434513
for (;;)
435514
{
436-
push_class_or_const_table(L, -1); // Stack: mt, mt, class table (ct) | nil
515+
push_class_or_const_table(L, -1); // Stack: mt, mt, ct | nil
437516
if (! lua_istable(L, -1)) // Stack: mt, mt, nil
438517
{
439518
lua_pop(L, 2); // Stack: mt
440519
return std::nullopt;
441520
}
442521

443-
lua_pushvalue(L, 2); // Stack: mt, mt, ct | co, field name
444-
lua_rawget(L, -2); // Stack: mt, mt, ct | co, field | nil
522+
lua_pushvalue(L, 2); // Stack: mt, mt, ct, field name
523+
lua_rawget(L, -2); // Stack: mt, mt, ct, field | nil
445524

446-
if (! lua_isnil(L, -1)) // Stack: mt, mt, ct | co, field
525+
if (! lua_isnil(L, -1)) // Stack: mt, mt, ct, field
447526
{
448527
if (! lua_iscfunction(L, -1))
449528
{
450529
lua_pop(L, 1);
451530
break;
452531
}
453532

454-
// Obtain class options
455-
const Options options = get_class_options(L, -2); // Stack: mt, mt, ct | co, field
533+
const Options options = get_class_options(L, -2); // Stack: mt, mt, ct, field
456534
if (! options.test(allowOverridingMethods))
457535
luaL_error(L, "immutable member '%s'", key);
458536

459-
rawsetfield(L, -2, make_super_method_name(key).c_str()); // Stack: mt, mt, ct | co
537+
rawsetfield(L, -2, make_super_method_name(key).c_str()); // Stack: mt, mt, ct
460538
break;
461539
}
462540

463-
lua_pop(L, 1); // Stack: mt, mt, ct | co
541+
lua_pop(L, 1); // Stack: mt, mt, ct
464542

465-
lua_rawgetp_x(L, -2, getParentKey()); // Stack: mt, mt, ct | co, parent mt (pmt) | nil
466-
if (lua_isnil(L, -1)) // Stack: mt, mt, ct | co, nil
543+
lua_rawgetp_x(L, -2, getParentKey()); // Stack: mt, mt, ct, pmt | nil
544+
if (lua_isnil(L, -1)) // Stack: mt, mt, ct, nil
467545
{
468-
lua_pop(L, 1); // Stack: mt, mt, ct | co
546+
lua_pop(L, 1); // Stack: mt, mt, ct
469547
break;
470548
}
471549

472-
LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, mt, ct | co, pmt
550+
LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt, mt, ct, pmt
473551
lua_remove(L, -2); // Stack: mt, mt, pmt
474552
lua_remove(L, -2); // Stack: mt, pmt
475553
}
476554

477-
lua_remove(L, -2); // Stack: mt, ct | co
478-
lua_getmetatable(L, -1); // Stack: mt, ct | co, mt2
479-
lua_pushvalue(L, 3); // Stack: mt, ct | co, mt2, arg3
480-
rawsetfield(L, -2, key); // Stack: mt, ct | co, mt2
481-
555+
lua_remove(L, -2); // Stack: mt, ct
556+
lua_getmetatable(L, -1); // Stack: mt, ct, ct_mt
557+
lua_pushvalue(L, 3); // Stack: mt, ct, ct_mt, arg3
558+
rawsetfield(L, -2, key); // Stack: mt, ct, ct_mt
482559
lua_pop(L, 2); // Stack: mt
483-
484560
return 0;
485561
}
486562

Tests/Source/ClassExtensibleTests.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,89 @@ TEST_F(ClassExtensibleTests, ExtensibleDerivedOverrideOneFunctionCallBaseForTheO
518518
EXPECT_EQ(103, result<int>());
519519
}
520520

521+
TEST_F(ClassExtensibleTests, ExtensibleDerivedDoesNotPollutBaseMethod)
522+
{
523+
// Regression test: defining a Lua method on a derived class must NOT overwrite the
524+
// same-named method on the base class (the originally reported inheritance bug).
525+
luabridge::getGlobalNamespace(L)
526+
.beginClass<ExtensibleBase>("ExtensibleBase", luabridge::extensibleClass)
527+
.addConstructor<void(*)()>()
528+
.endClass()
529+
.deriveClass<ExtensibleDerived, ExtensibleBase>("ExtensibleDerived", luabridge::extensibleClass)
530+
.addConstructor<void(*)()>()
531+
.endClass()
532+
;
533+
534+
runLua(R"(
535+
function ExtensibleBase:init() return 100 end
536+
function ExtensibleDerived:init() return 200 end
537+
538+
local base = ExtensibleBase()
539+
local derived = ExtensibleDerived()
540+
result = base:init()
541+
)");
542+
543+
// base:init() must return 100 (the Base version), not 200 (the Derived version)
544+
EXPECT_EQ(100, result<int>());
545+
}
546+
547+
TEST_F(ClassExtensibleTests, ExtensibleDerivedMethodIsolatedFromBase)
548+
{
549+
// Verify both sides independently after same-name Lua methods are defined on each class.
550+
luabridge::getGlobalNamespace(L)
551+
.beginClass<ExtensibleBase>("ExtensibleBase", luabridge::extensibleClass)
552+
.addConstructor<void(*)()>()
553+
.endClass()
554+
.deriveClass<ExtensibleDerived, ExtensibleBase>("ExtensibleDerived", luabridge::extensibleClass)
555+
.addConstructor<void(*)()>()
556+
.endClass()
557+
;
558+
559+
runLua(R"(
560+
function ExtensibleBase:getValue() return 10 end
561+
function ExtensibleDerived:getValue() return 20 end
562+
563+
local base = ExtensibleBase()
564+
local derived = ExtensibleDerived()
565+
result = derived:getValue() * 100 + base:getValue()
566+
)");
567+
568+
// derived:getValue() == 20, base:getValue() == 10 => 20*100 + 10 == 2010
569+
EXPECT_EQ(2010, result<int>());
570+
}
571+
572+
TEST_F(ClassExtensibleTests, ExtensibleDerivedOverridePreservesCppBaseMethod)
573+
{
574+
// When allowOverridingMethods is set and a derived class overrides a C++ method,
575+
// the base class should still call the original C++ implementation.
576+
constexpr auto options = luabridge::extensibleClass | luabridge::allowOverridingMethods;
577+
578+
luabridge::getGlobalNamespace(L)
579+
.beginClass<ExtensibleBase>("ExtensibleBase", options)
580+
.addConstructor<void(*)()>()
581+
.addFunction("baseClass", &ExtensibleBase::baseClass)
582+
.endClass()
583+
.deriveClass<ExtensibleDerived, ExtensibleBase>("ExtensibleDerived", options)
584+
.addConstructor<void(*)()>()
585+
.endClass()
586+
;
587+
588+
runLua(R"(
589+
-- Override baseClass only on ExtensibleDerived; base must remain unaffected
590+
function ExtensibleDerived:baseClass()
591+
return 100 + self:super_baseClass()
592+
end
593+
594+
local base = ExtensibleBase()
595+
local derived = ExtensibleDerived()
596+
result = derived:baseClass() * 1000 + base:baseClass()
597+
)");
598+
599+
// derived:baseClass() == 100 + 1 == 101, base:baseClass() == 1 (original C++)
600+
// => 101 * 1000 + 1 == 101001
601+
EXPECT_EQ(101001, result<int>());
602+
}
603+
521604
TEST_F(ClassExtensibleTests, ExtensibleClassCustomMetamethods)
522605
{
523606
auto options = luabridge::allowOverridingMethods | luabridge::extensibleClass;

0 commit comments

Comments
 (0)