diff --git a/.gitignore b/.gitignore index 948f96c2..ec4c7da8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ Makefile CMakeCache.txt CMakeFiles/ build/ +Build/ *.dir/ *.sln *.vcxproj diff --git a/Distribution/LuaBridge/LuaBridge.h b/Distribution/LuaBridge/LuaBridge.h index 96fc33ac..8e373dd8 100644 --- a/Distribution/LuaBridge/LuaBridge.h +++ b/Distribution/LuaBridge/LuaBridge.h @@ -2746,6 +2746,16 @@ template ().find_first_of('.')> return reinterpret_cast(0x8108); } +[[nodiscard]] inline const void* getStaticIndexFallbackKey() +{ + return reinterpret_cast(0x81cc); +} + +[[nodiscard]] inline const void* getStaticNewIndexFallbackKey() +{ + return reinterpret_cast(0x8109); +} + template [[nodiscard]] const void* getStaticRegistryKey() noexcept { @@ -6188,6 +6198,30 @@ inline std::optional try_call_index_fallback(lua_State* L) return std::nullopt; } +inline std::optional try_call_static_index_fallback(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + lua_rawgetp_x(L, -1, getStaticIndexFallbackKey()); + if (! lua_iscfunction(L, -1)) + { + lua_pop(L, 1); + return std::nullopt; + } + + lua_pushvalue(L, 2); + lua_call(L, 1, 1); + + if (! lua_isnoneornil(L, -1)) + { + lua_remove(L, -2); + return 1; + } + + lua_pop(L, 1); + return std::nullopt; +} + template inline std::optional try_call_index_extensible(lua_State* L, const char* key) { @@ -6239,6 +6273,12 @@ inline int index_metamethod(lua_State* L) if (auto result = try_call_index_fallback(L)) return *result; } + else + { + + if (auto result = try_call_static_index_fallback(L)) + return *result; + } if (lua_istable(L, 1)) { @@ -6354,6 +6394,24 @@ inline std::optional try_call_newindex_fallback(lua_State* L) return 0; } +inline std::optional try_call_static_newindex_fallback(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + lua_rawgetp_x(L, -1, getStaticNewIndexFallbackKey()); + if (! lua_iscfunction(L, -1)) + { + lua_pop(L, 1); + return std::nullopt; + } + + lua_pushvalue(L, 2); + lua_pushvalue(L, 3); + lua_call(L, 2, 0); + + return 0; +} + inline std::optional try_call_newindex_extensible(lua_State* L, const char* key) { LUABRIDGE_ASSERT(key != nullptr); @@ -6525,6 +6583,9 @@ inline int newindex_metamethod(lua_State* L) else { + if (auto result = try_call_static_newindex_fallback(L)) + return *result; + if (options.test(extensibleClass)) { if (auto result = try_call_newindex_extensible(L, key)) @@ -9600,6 +9661,64 @@ class Namespace : public detail::Registrar return *this; } + template + auto addStaticIndexMetaMethod(Function function) + -> std::enable_if_t + && std::is_invocable_v, Class&> + { + using FnType = decltype(function); + + assertStackState(); + + lua_newuserdata_aligned(L, std::move(function)); + lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__index", 1); + lua_rawsetp_x(L, -2, detail::getStaticIndexFallbackKey()); + + return *this; + } + + Class& addStaticIndexMetaMethod(LuaRef (*idxf)(const LuaRef&, lua_State*)) + { + using FnType = decltype(idxf); + + assertStackState(); + + lua_pushlightuserdata(L, reinterpret_cast(idxf)); + lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__index", 1); + lua_rawsetp_x(L, -2, detail::getStaticIndexFallbackKey()); + + return *this; + } + + template + auto addStaticNewIndexMetaMethod(Function function) + -> std::enable_if_t + && std::is_invocable_v, Class&> + { + using FnType = decltype(function); + + assertStackState(); + + lua_newuserdata_aligned(L, std::move(function)); + lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__newindex", 1); + lua_rawsetp_x(L, -2, detail::getStaticNewIndexFallbackKey()); + + return *this; + } + + Class& addStaticNewIndexMetaMethod(LuaRef (*idxf)(const LuaRef&, const LuaRef&, lua_State*)) + { + using FnType = decltype(idxf); + + assertStackState(); + + lua_pushlightuserdata(L, reinterpret_cast(idxf)); + lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__newindex", 1); + lua_rawsetp_x(L, -2, detail::getStaticNewIndexFallbackKey()); + + return *this; + } + template Class& addProperty(const char* name, Getter getter, bool) = delete; diff --git a/Manual.md b/Manual.md index 2c10e55f..1e311ee2 100644 --- a/Manual.md +++ b/Manual.md @@ -38,6 +38,7 @@ Contents * [2.7 - Extending Classes](#27---extending-classes) * [2.7.1 - Extensible Classes](#271---extensible-classes) * [2.7.2 - Index and New Index Metamethods Fallback](#272---index-and-new-index-metamethods-fallback) + * [2.7.3 - Static Index and New Index Metamethods Fallback](#273---static-index-and-new-index-metamethods-fallback) * [2.8 - Lua Stack](#28---lua-stack) * [2.8.1 - Enums](#281---enums) * [2.8.2 - lua_State](#282---lua_state) @@ -881,6 +882,73 @@ propertyOne = flexi.propertyOne assert (propertyOne == 1337, "Value is now present !") ``` +### 2.7.3 - Static Index and New Index Metamethods Fallback + +The same fallback mechanism is available for the *static class table* — i.e. for key lookups performed on the class name itself (e.g. `MyClass.someKey`) rather than on an instance. Use `addStaticIndexMetaMethod` and `addStaticNewIndexMetaMethod` to register the callbacks. Unlike their instance counterparts, the static callbacks receive only the key (and optionally `lua_State*`) — there is no `self` parameter. + +```cpp +struct MyClass {}; + +std::unordered_map store; + +luabridge::getGlobalNamespace (L) + .beginClass ("MyClass") + .addStaticIndexMetaMethod ([] (const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef + { + auto it = store.find (key.tostring ()); + if (it != store.end ()) + return luabridge::LuaRef (L, it->second); + + return luabridge::LuaRef (L, luabridge::LuaNil ()); + }) + .addStaticNewIndexMetaMethod ([] (const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State* L) -> luabridge::LuaRef + { + if (value.isNumber ()) + store[key.tostring ()] = value.unsafe_cast (); + return value; + }) + .endClass (); +``` + +Then in lua: + +```lua +MyClass.dynamicProp = 42 + +value = MyClass.dynamicProp +assert (value == 42, "Value stored via static __newindex fallback and retrieved via static __index fallback") + +missing = MyClass.nonExistingKey +assert (missing == nil, "Unknown key returns nil through the static __index fallback") +``` + +Existing static properties and functions registered with `addStaticProperty` / `addStaticFunction` are found *after* the fallback is consulted. If the fallback callback returns `nil` (or a nil `LuaRef`) for a given key, normal lookup continues and the real property or function is returned. Conversely, if the callback returns a non-nil value the fallback result takes priority over any registered static property with the same name. + +```cpp +struct MyClass +{ + static int answer () { return 42; } +}; + +luabridge::getGlobalNamespace (L) + .beginClass ("MyClass") + .addStaticFunction ("answer", &MyClass::answer) + .addStaticIndexMetaMethod ([] (const luabridge::LuaRef& /*key*/, lua_State* L) -> luabridge::LuaRef + { + // Returning nil lets the registered static function be found normally + return luabridge::LuaRef (L, luabridge::LuaNil ()); + }) + .endClass (); +``` + +Then in lua: + +```lua +-- The registered static function is still callable because the fallback returned nil +result = MyClass.answer () +assert (result == 42) +``` + 2.8 - Lua Stack --------------- diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index 65d6ac85..9cdc95b4 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -240,6 +240,30 @@ inline std::optional try_call_index_fallback(lua_State* L) return std::nullopt; } +inline std::optional try_call_static_index_fallback(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt + + lua_rawgetp_x(L, -1, getStaticIndexFallbackKey()); // Stack: mt, ifb (may be nil) + if (! lua_iscfunction(L, -1)) + { + lua_pop(L, 1); // Stack: mt + return std::nullopt; + } + + lua_pushvalue(L, 2); // Stack: mt, ifb, arg1 (key only, no self for static) + lua_call(L, 1, 1); // Stack: mt, ifbresult + + if (! lua_isnoneornil(L, -1)) + { + lua_remove(L, -2); // Stack: ifbresult + return 1; + } + + lua_pop(L, 1); // Stack: mt + return std::nullopt; +} + template inline std::optional try_call_index_extensible(lua_State* L, const char* key) { @@ -292,6 +316,12 @@ inline int index_metamethod(lua_State* L) if (auto result = try_call_index_fallback(L)) return *result; } + else + { + // Repeat the lookup in the static index fallback + if (auto result = try_call_static_index_fallback(L)) + return *result; + } // Search into self or metatable if (lua_istable(L, 1)) @@ -424,6 +454,24 @@ inline std::optional try_call_newindex_fallback(lua_State* L) return 0; } +inline std::optional try_call_static_newindex_fallback(lua_State* L) +{ + LUABRIDGE_ASSERT(lua_istable(L, -1)); // Stack: mt + + lua_rawgetp_x(L, -1, getStaticNewIndexFallbackKey()); // Stack: mt, nifb (may be nil) + if (! lua_iscfunction(L, -1)) + { + lua_pop(L, 1); // Stack: mt + return std::nullopt; + } + + lua_pushvalue(L, 2); // stack: mt, nifb, arg1 (key only, no self for static) + lua_pushvalue(L, 3); // stack: mt, nifb, arg1, arg2 (value) + lua_call(L, 2, 0); // stack: mt + + return 0; +} + inline std::optional try_call_newindex_extensible(lua_State* L, const char* key) { LUABRIDGE_ASSERT(key != nullptr); @@ -610,6 +658,10 @@ inline int newindex_metamethod(lua_State* L) } else { + // Try in the static new index fallback + if (auto result = try_call_static_newindex_fallback(L)) + return *result; + // Try in the new index extensible if (options.test(extensibleClass)) { diff --git a/Source/LuaBridge/detail/ClassInfo.h b/Source/LuaBridge/detail/ClassInfo.h index acd11723..b1ee1aa8 100644 --- a/Source/LuaBridge/detail/ClassInfo.h +++ b/Source/LuaBridge/detail/ClassInfo.h @@ -165,6 +165,24 @@ template ().find_first_of('.')> return reinterpret_cast(0x8108); } +//================================================================================================= +/** + * The key of the static index fall back in another metatable. + */ +[[nodiscard]] inline const void* getStaticIndexFallbackKey() +{ + return reinterpret_cast(0x81cc); +} + +//================================================================================================= +/** + * The key of the static new index fall back in another metatable. + */ +[[nodiscard]] inline const void* getStaticNewIndexFallbackKey() +{ + return reinterpret_cast(0x8109); +} + //================================================================================================= /** * @brief Get the key for the static table in the Lua registry. diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 74455ffa..3932710c 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -551,6 +551,76 @@ class Namespace : public detail::Registrar return *this; } + //========================================================================================= + /** + * @brief Add a static index metamethod function fallback that is triggered when no result is found in static functions, properties or any other static members. + * + * Let the user define a fallback index (__index) metamethod for the static class table. + */ + template + auto addStaticIndexMetaMethod(Function function) + -> std::enable_if_t + && std::is_invocable_v, Class&> + { + using FnType = decltype(function); + + assertStackState(); // Stack: const table (co), class table (cl), static table (st) + + lua_newuserdata_aligned(L, std::move(function)); // Stack: co, cl, st, function userdata (ud) + lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__index", 1); // Stack: co, cl, st, function + lua_rawsetp_x(L, -2, detail::getStaticIndexFallbackKey()); + + return *this; + } + + Class& addStaticIndexMetaMethod(LuaRef (*idxf)(const LuaRef&, lua_State*)) + { + using FnType = decltype(idxf); + + assertStackState(); // Stack: const table (co), class table (cl), static table (st) + + lua_pushlightuserdata(L, reinterpret_cast(idxf)); // Stack: co, cl, st, function ptr + lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__index", 1); // Stack: co, cl, st, function + lua_rawsetp_x(L, -2, detail::getStaticIndexFallbackKey()); + + return *this; + } + + //========================================================================================= + /** + * @brief Add a static new index metamethod function fallback that is triggered when no result is found in static functions, properties or any other static members. + * + * Let the user define a fallback new index (__newindex) metamethod for the static class table. + */ + template + auto addStaticNewIndexMetaMethod(Function function) + -> std::enable_if_t + && std::is_invocable_v, Class&> + { + using FnType = decltype(function); + + assertStackState(); // Stack: const table (co), class table (cl), static table (st) + + lua_newuserdata_aligned(L, std::move(function)); // Stack: co, cl, st, function userdata (ud) + lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__newindex", 1); // Stack: co, cl, st, function + lua_rawsetp_x(L, -2, detail::getStaticNewIndexFallbackKey()); + + return *this; + } + + Class& addStaticNewIndexMetaMethod(LuaRef (*idxf)(const LuaRef&, const LuaRef&, lua_State*)) + { + using FnType = decltype(idxf); + + assertStackState(); // Stack: const table (co), class table (cl), static table (st) + + lua_pushlightuserdata(L, reinterpret_cast(idxf)); // Stack: co, cl, st, function ptr + lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__newindex", 1); // Stack: co, cl, st, function + lua_rawsetp_x(L, -2, detail::getStaticNewIndexFallbackKey()); + + return *this; + } + //========================================================================================= /** * @brief Add or replace a property member, by constructible by std::function. diff --git a/Tests/Source/ClassExtensibleTests.cpp b/Tests/Source/ClassExtensibleTests.cpp index ba406080..19eaa8a7 100644 --- a/Tests/Source/ClassExtensibleTests.cpp +++ b/Tests/Source/ClassExtensibleTests.cpp @@ -883,3 +883,276 @@ TEST_F(ClassExtensibleTests, MetatablePrinting) EXPECT_FALSE(res.unsafe_cast().empty()); } #endif + +namespace { +struct StaticMetamethodClass +{ + StaticMetamethodClass() = default; +}; + +luabridge::LuaRef staticIndexFallbackFunction(const luabridge::LuaRef& key, lua_State* L) +{ + if (key.tostring() == "xyz") + { + if (!luabridge::push(L, 42)) + lua_pushnil(L); + + return luabridge::LuaRef::fromStack(L); + } + + return luabridge::LuaRef(L); +} + +luabridge::LuaRef staticNewIndexFallbackFunction(const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State* L) +{ + return value; +} +} // namespace + +TEST_F(ClassExtensibleTests, StaticIndexFallbackMetaMethodFunctionPtr) +{ + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticIndexMetaMethod(&staticIndexFallbackFunction) + .endClass(); + + runLua("result = X.xyz"); + ASSERT_EQ(42, result()); +} + +TEST_F(ClassExtensibleTests, StaticIndexFallbackMetaMethodFreeFunctor) +{ + std::string capture = "hello"; + + auto indexMetaMethod = [&capture](const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef + { + if (key.tostring() == "greeting") + { + if (!luabridge::push(L, capture)) + lua_pushnil(L); + return luabridge::LuaRef::fromStack(L); + } + + return luabridge::LuaRef(L); + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticIndexMetaMethod(indexMetaMethod) + .endClass(); + + runLua("result = X.greeting"); + ASSERT_EQ("hello", result()); +} + +TEST_F(ClassExtensibleTests, StaticNewIndexFallbackMetaMethodFunctionPtr) +{ + // Verify that assigning to an unknown static key invokes the newindex fallback + // (staticNewIndexFallbackFunction is a plain function pointer that accepts the assignment). + // Without the fallback, assigning to an unknown key would raise a Lua error. + // We also confirm reading the key still goes through the index fallback. + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticIndexMetaMethod(&staticIndexFallbackFunction) + .addStaticNewIndexMetaMethod(&staticNewIndexFallbackFunction) + .endClass(); + + // staticIndexFallbackFunction always returns 42 for "xyz" + ASSERT_TRUE(runLua("X.xyz = 100")); // must not error: newindex fallback handles it + runLua("result = X.xyz"); + ASSERT_EQ(42, result()); // index fallback still returns 42 regardless of assignment +} + +TEST_F(ClassExtensibleTests, StaticNewIndexFallbackMetaMethodFreeFunctor) +{ + std::unordered_map data; + + auto indexMetaMethod = [&data](const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef + { + auto it = data.find(key.tostring()); + if (it != data.end()) + { + if (!luabridge::push(L, it->second)) + lua_pushnil(L); + return luabridge::LuaRef::fromStack(L); + } + + return luabridge::LuaRef(L); + }; + + auto newIndexMetaMethod = [&data](const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State*) -> luabridge::LuaRef + { + if (value.isNumber()) + data.emplace(key.tostring(), value.unsafe_cast()); + return value; + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticIndexMetaMethod(indexMetaMethod) + .addStaticNewIndexMetaMethod(newIndexMetaMethod) + .endClass(); + + runLua("X.foo = 111; X.bar = 222; result = X.foo + X.bar"); + ASSERT_EQ(333, result()); +} + +TEST_F(ClassExtensibleTests, StaticIndexFallbackReturnsNilForUnknownKey) +{ + auto indexMetaMethod = [](const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef + { + if (key.tostring() == "known") + { + if (!luabridge::push(L, 77)) + lua_pushnil(L); + return luabridge::LuaRef::fromStack(L); + } + + return luabridge::LuaRef(L); + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticIndexMetaMethod(indexMetaMethod) + .endClass(); + + runLua("result = X.known"); + ASSERT_EQ(77, result()); + + runLua("result = X.unknown"); + ASSERT_TRUE(result().isNil()); +} + +TEST_F(ClassExtensibleTests, StaticIndexFallbackFallsThroughToExistingStaticProperty) +{ + // When the fallback returns nil for a key, the real static property must still be found. + int staticValue = 99; + + auto indexMetaMethod = [](const luabridge::LuaRef& /*key*/, lua_State* L) -> luabridge::LuaRef + { + // Always return nil so normal lookup proceeds + return luabridge::LuaRef(L); + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticProperty("answer", [&staticValue] { return staticValue; }) + .addStaticIndexMetaMethod(indexMetaMethod) + .endClass(); + + runLua("result = X.answer"); + ASSERT_EQ(99, result()); +} + +TEST_F(ClassExtensibleTests, StaticIndexFallbackFallsThroughToExistingStaticFunction) +{ + // When the fallback returns nil for a key, an existing static function must still be callable. + auto indexMetaMethod = [](const luabridge::LuaRef& /*key*/, lua_State* L) -> luabridge::LuaRef + { + return luabridge::LuaRef(L); + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticFunction("compute", []() { return 7; }) + .addStaticIndexMetaMethod(indexMetaMethod) + .endClass(); + + runLua("result = X.compute()"); + ASSERT_EQ(7, result()); +} + +TEST_F(ClassExtensibleTests, StaticIndexFallbackCanShadowExistingStaticProperty) +{ + // When the fallback returns a non-nil value for a key that also exists as a static + // property, the fallback result takes priority. + int staticValue = 10; + + auto indexMetaMethod = [](const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef + { + if (key.tostring() == "answer") + { + if (!luabridge::push(L, 999)) + lua_pushnil(L); + return luabridge::LuaRef::fromStack(L); + } + return luabridge::LuaRef(L); + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticProperty("answer", [&staticValue] { return staticValue; }) + .addStaticIndexMetaMethod(indexMetaMethod) + .endClass(); + + // Fallback returns 999 for "answer", not the registered property value (10) + runLua("result = X.answer"); + ASSERT_EQ(999, result()); +} + +TEST_F(ClassExtensibleTests, StaticNewIndexFallbackStandaloneWithoutIndexFallback) +{ + // newindex fallback should work even when no index fallback is registered. + // Assigning to an unknown key must not raise an error. + bool callbackInvoked = false; + + auto newIndexMetaMethod = [&callbackInvoked](const luabridge::LuaRef& /*key*/, const luabridge::LuaRef& value, lua_State*) -> luabridge::LuaRef + { + callbackInvoked = true; + return value; + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticNewIndexMetaMethod(newIndexMetaMethod) + .endClass(); + + ASSERT_TRUE(runLua("X.whatever = 42")); + ASSERT_TRUE(callbackInvoked); +} + +TEST_F(ClassExtensibleTests, StaticIndexAndNewIndexFallbackCoexistWithStaticProperty) +{ + // Register both fallbacks alongside a real static property. + // The fallback store handles unknown keys; the real property is unaffected. + std::unordered_map store; + int staticValue = 55; + + auto indexMetaMethod = [&store](const luabridge::LuaRef& key, lua_State* L) -> luabridge::LuaRef + { + auto it = store.find(key.tostring()); + if (it != store.end()) + { + if (!luabridge::push(L, it->second)) + lua_pushnil(L); + return luabridge::LuaRef::fromStack(L); + } + return luabridge::LuaRef(L); + }; + + auto newIndexMetaMethod = [&store](const luabridge::LuaRef& key, const luabridge::LuaRef& value, lua_State*) -> luabridge::LuaRef + { + if (value.isNumber()) + store[key.tostring()] = value.unsafe_cast(); + return value; + }; + + luabridge::getGlobalNamespace(L) + .beginClass("X") + .addStaticProperty("base", [&staticValue] { return staticValue; }) + .addStaticIndexMetaMethod(indexMetaMethod) + .addStaticNewIndexMetaMethod(newIndexMetaMethod) + .endClass(); + + // Real static property is accessible + runLua("result = X.base"); + ASSERT_EQ(55, result()); + + // Unknown key goes through the fallback store + runLua("X.extra = 100; result = X.extra"); + ASSERT_EQ(100, result()); + + // Real property is still accessible after fallback store mutations + runLua("result = X.base"); + ASSERT_EQ(55, result()); +}