diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index 2b44b340..418aea70 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -254,6 +254,37 @@ inline bool is_metamethod(std::string_view method_name) return result != metamethods.end() && *result == method_name; } +/** + * @brief Check if a method name is a binary operator metamethod. + */ +inline bool is_binary_operator_metamethod(std::string_view method_name) +{ + static constexpr auto metamethods = make_array( + "__add", + "__band", + "__bor", + "__bxor", + "__concat", + "__div", + "__eq", + "__idiv", + "__le", + "__lt", + "__mod", + "__mul", + "__pow", + "__shl", + "__shr", + "__sub" + ); + + if (method_name.size() <= 2 || method_name[0] != '_' || method_name[1] != '_') + return false; + + auto result = std::lower_bound(metamethods.begin(), metamethods.end(), method_name); + return result != metamethods.end() && *result == method_name; +} + inline void rawset_super_method(lua_State* L, int tableIndex, const char* key) { LUABRIDGE_ASSERT(key != nullptr); @@ -2251,6 +2282,18 @@ bool overload_type_checker(lua_State* L, int start) return overload_check_args(L, start); } +/** + * @brief Type checker for reversed proxy functions (binary operators with swapped operands). + * + * Checks all arguments from absolute stack index 1, ignoring @p start, because the class object + * is not the first operand on the stack (eg. `3 * vec`). + */ +template +bool reversed_overload_type_checker(lua_State* L, int) +{ + return overload_check_args(L, 1); +} + //================================================================================================= /** * @brief lua_CFunction to resolve an invocation between several overloads. @@ -2479,7 +2522,8 @@ template >> void push_member_function(lua_State* L, F&& f, const char* debugname) { - static_assert(std::is_same_v>>>); + static_assert(std::is_same_v>>> || + is_reversed_proxy_function_v); lua_newuserdata_aligned(L, std::forward(f)); lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 1); diff --git a/Source/LuaBridge/detail/FuncTraits.h b/Source/LuaBridge/detail/FuncTraits.h index a8ea4371..8eb6d104 100644 --- a/Source/LuaBridge/detail/FuncTraits.h +++ b/Source/LuaBridge/detail/FuncTraits.h @@ -541,6 +541,28 @@ inline static constexpr bool is_const_proxy_function_v = is_proxy_member_function_v && std::is_const_v>>>; +//================================================================================================= +/** + * @brief A constexpr check for reversed proxy member functions (binary operators). + * + * A reversed proxy function is a binary callable where T is the second argument instead of the + * first, as needed by binary operator metamethods invoked with swapped operands (eg. `3 * vec`). + * + * @tparam T Type where the callable should be able to operate. + * @tparam F Callable object. + */ +template +inline static constexpr bool is_reversed_proxy_function_v = + !std::is_member_function_pointer_v && + !is_proxy_member_function_v && + function_arity_v == 2 && + std::is_same_v>>>; + +template +inline static constexpr bool is_const_reversed_proxy_function_v = + is_reversed_proxy_function_v && + std::is_const_v>>>; + //================================================================================================= /** * @brief An integral constant expression that gives the number of arguments excluding one type (usually used with lua_State*) accepted by the callable object. @@ -593,7 +615,8 @@ inline static constexpr std::size_t member_function_arity_excluding_v = member_f template static constexpr bool is_const_function = detail::is_const_member_function_pointer_v || - (detail::function_arity_v > 0 && detail::is_const_proxy_function_v); + (detail::function_arity_v > 0 && detail::is_const_proxy_function_v) || + detail::is_const_reversed_proxy_function_v; template inline static constexpr std::size_t const_functions_count = (0 + ... + (is_const_function ? 1 : 0)); diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 1b787257..6d59cde0 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -950,6 +950,15 @@ class Namespace : public detail::Registrar #endif } + if constexpr ((detail::is_reversed_proxy_function_v || ...)) + { + if (!detail::is_binary_operator_metamethod(name)) + { + throw_or_assert("reversed functions are only allowed on binary operator metamethods"); + return *this; + } + } + if constexpr (sizeof...(Functions) == 1) { ([&] @@ -995,6 +1004,12 @@ class Namespace : public detail::Registrar entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; } + else if constexpr (detail::is_reversed_proxy_function_v) + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::member_function_arity_excluding_v) - 1; + entry.checker = &detail::reversed_overload_type_checker; + } else { using ArgsPack = detail::function_arguments_t; @@ -1052,6 +1067,12 @@ class Namespace : public detail::Registrar entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; } + else if constexpr (detail::is_reversed_proxy_function_v) + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::member_function_arity_excluding_v) - 1; + entry.checker = &detail::reversed_overload_type_checker; + } else { using ArgsPack = detail::function_arguments_t; diff --git a/Tests/Source/ClassTests.cpp b/Tests/Source/ClassTests.cpp index bf86d144..8b2e2a52 100644 --- a/Tests/Source/ClassTests.cpp +++ b/Tests/Source/ClassTests.cpp @@ -169,6 +169,67 @@ class Bar Foo foo; }; +class Vec2 +{ +public: + Vec2(float x, float y) + : x(x), y(y) + { + } + + friend Vec2 operator*(const Vec2& vec, float scalar) + { + return Vec2(vec.x * scalar, vec.y * scalar); + } + + friend Vec2 operator*(float scalar, const Vec2& vec) + { + return Vec2(vec.x * scalar, vec.y * scalar); + } + + friend Vec2 operator+(const Vec2& vec, float scalar) + { + return Vec2(vec.x + scalar, vec.y + scalar); + } + + friend Vec2 operator+(float scalar, const Vec2& vec) + { + return Vec2(scalar + vec.x, scalar + vec.y); + } + + friend Vec2 operator-(const Vec2& vec, float scalar) + { + return Vec2(vec.x - scalar, vec.y - scalar); + } + + friend Vec2 operator-(float scalar, const Vec2& vec) + { + return Vec2(scalar - vec.x, scalar - vec.y); + } + + friend Vec2 operator/(const Vec2& vec, float scalar) + { + return Vec2(vec.x / scalar, vec.y / scalar); + } + + friend Vec2 operator/(float scalar, const Vec2& vec) + { + return Vec2(scalar / vec.x, scalar / vec.y); + } + + friend bool operator==(const Vec2& lhs, const Vec2& rhs) + { + return lhs.x == rhs.x && lhs.y == rhs.y; + } + + float getX() const { return x; } + float getY() const { return y; } + +private: + float x; + float y; +}; + } // namespace TEST_F(ClassTests, Assignment) @@ -2572,6 +2633,122 @@ TEST_F(ClassMetaMethods, __mul) ASSERT_EQ(10, result().data); } +TEST_F(ClassMetaMethods, ReversedAddOperator) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addConstructor() + .addFunction("__add", + [](const Vec2& vec, float scalar) { return vec + scalar; }, + [](float scalar, const Vec2& vec) { return scalar + vec; }) + .endClass(); + + runLua("result = Vec2 (1, 2) + 3"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(4, 5), result()); + + runLua("result = 3 + Vec2 (1, 2)"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(4, 5), result()); +} + +TEST_F(ClassMetaMethods, ReversedSubOperator) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addConstructor() + .addFunction("__sub", + [](const Vec2& vec, float scalar) { return vec - scalar; }, + [](float scalar, const Vec2& vec) { return scalar - vec; }) + .endClass(); + + runLua("result = Vec2 (5, 7) - 2"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 5), result()); + + runLua("result = 10 - Vec2 (1, 2)"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(9, 8), result()); +} + +TEST_F(ClassMetaMethods, ReversedMulOperator) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addConstructor() + .addFunction("__mul", + [](const Vec2& vec, float scalar) { return vec * scalar; }, + [](float scalar, const Vec2& vec) { return scalar * vec; }) + .endClass(); + + runLua("result = Vec2 (1, 2) * 3"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 6), result()); + + runLua("result = 3 * Vec2 (1, 2)"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 6), result()); +} + +TEST_F(ClassMetaMethods, ReversedDivOperator) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addConstructor() + .addFunction("__div", + [](const Vec2& vec, float scalar) { return vec / scalar; }, + [](float scalar, const Vec2& vec) { return scalar / vec; }) + .endClass(); + + runLua("result = Vec2 (6, 8) / 2"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 4), result()); + + runLua("result = 12 / Vec2 (2, 4)"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(6, 3), result()); +} + +TEST_F(ClassMetaMethods, ReversedOperatorOnConstInstance) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addConstructor() + .addFunction("__mul", + [](const Vec2& vec, float scalar) { return vec * scalar; }, + [](float scalar, const Vec2& vec) { return scalar * vec; }) + .endClass(); + + const Vec2 vec(1, 2); + luabridge::setGlobal(L, &vec, "v"); + + runLua("result = v * 3"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 6), result()); + + runLua("result = 3 * v"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 6), result()); +} + +TEST_F(ClassMetaMethods, ReversedOperatorOnly) +{ + luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addConstructor() + .addFunction("__mul", + [](float scalar, const Vec2& vec) { return scalar * vec; }) + .endClass(); + + runLua("result = 3 * Vec2 (1, 2)"); + ASSERT_TRUE(result().isUserdata()); + ASSERT_EQ(Vec2(3, 6), result()); + +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_ANY_THROW(runLua("result = Vec2 (1, 2) * 3")); +#endif +} + TEST_F(ClassMetaMethods, __div) { typedef Class Int; @@ -2776,6 +2953,24 @@ TEST_F(ClassMetaMethods, __gcForbidden) .endClass(), std::exception); } + +TEST_F(ClassMetaMethods, ReversedFunctionsForbiddenOutsideBinaryOperators) +{ + ASSERT_THROW(luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addFunction("mul", + [](const Vec2& vec, float scalar) { return vec * scalar; }, + [](float scalar, const Vec2& vec) { return scalar * vec; }) + .endClass(), + std::exception); + + ASSERT_THROW(luabridge::getGlobalNamespace(L) + .beginClass("Vec2") + .addFunction("__tostring", + [](float scalar, const Vec2& vec) { return scalar * vec; }) + .endClass(), + std::exception); +} #endif TEST_F(ClassMetaMethods, MetamethodsShouldNotBePartOfClassInstances)