From 928b04c150f2946fe2b0d33ab67c02fb788b7cdd Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sat, 28 Mar 2026 14:51:59 +0100 Subject: [PATCH 01/10] Benchmarks --- Benchmarks/CMakeLists.txt | 44 ++++++ Benchmarks/README.md | 60 +++++++ Benchmarks/benchmark_sol2_compare.cpp | 215 ++++++++++++++++++++++++++ CMakeLists.txt | 5 + Source/LuaBridge/detail/LuaHelpers.h | 5 +- Source/LuaBridge/detail/Stack.h | 4 +- 6 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 Benchmarks/CMakeLists.txt create mode 100644 Benchmarks/README.md create mode 100644 Benchmarks/benchmark_sol2_compare.cpp diff --git a/Benchmarks/CMakeLists.txt b/Benchmarks/CMakeLists.txt new file mode 100644 index 00000000..0a06913d --- /dev/null +++ b/Benchmarks/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.10) + +include(FetchContent) + +set(LUABRIDGE_BENCHMARK_SOURCES + benchmark_sol2_compare.cpp + ../Tests/Lua/LuaLibrary5.4.8.cpp +) + +add_executable(LuaBridgeBenchmarks ${LUABRIDGE_BENCHMARK_SOURCES}) + +target_include_directories(LuaBridgeBenchmarks PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/.. + ${CMAKE_CURRENT_LIST_DIR}/../Source + ${CMAKE_CURRENT_LIST_DIR}/../Tests + ${CMAKE_CURRENT_LIST_DIR}/../Tests/Lua/Lua.5.4.8/src +) + +target_compile_definitions(LuaBridgeBenchmarks PRIVATE + LUABRIDGE_BENCHMARK_LUA54=1 + LUABRIDGE_TEST_LUA_VERSION=504 +) + +set(LUABRIDGE_BENCHMARK_WITH_SOL2 OFF CACHE BOOL "Enable optional sol2 comparison in benchmark executable") + +if (LUABRIDGE_BENCHMARK_WITH_SOL2) + set(LUABRIDGE_SOL2_GIT_REPOSITORY "https://github.com/ThePhD/sol2.git" CACHE STRING "sol2 repository URL") + set(LUABRIDGE_SOL2_GIT_TAG "v3.3.0" CACHE STRING "sol2 git tag or commit") + + # sol2 is header-only; FetchContent gives us a reproducible version without manual setup. + FetchContent_Declare( + sol2 + GIT_REPOSITORY ${LUABRIDGE_SOL2_GIT_REPOSITORY} + GIT_TAG ${LUABRIDGE_SOL2_GIT_TAG} + ) + FetchContent_MakeAvailable(sol2) + + target_include_directories(LuaBridgeBenchmarks PRIVATE ${sol2_SOURCE_DIR}/include) + target_compile_definitions(LuaBridgeBenchmarks PRIVATE + LUABRIDGE_BENCHMARK_WITH_SOL2=1 + SOL_STD_OPTIONAL=1 + SOL_LUA_VERSION=504 + ) +endif () diff --git a/Benchmarks/README.md b/Benchmarks/README.md new file mode 100644 index 00000000..b684dceb --- /dev/null +++ b/Benchmarks/README.md @@ -0,0 +1,60 @@ +# LuaBridge Benchmarks + +This directory contains a standalone benchmark executable focused on LuaBridge call and property access overhead. The selected cases mirror the style of the sol2 benchmark categories (Lua -> C++ calls, C++ -> Lua calls, member calls, and property access). + +## Build + +From project root: + +```bash +cmake -S . -B Build -DCMAKE_BUILD_TYPE=Release -DLUABRIDGE_BENCHMARKS=ON +cmake --build Build --target LuaBridgeBenchmarks --config Release +``` + +## Run + +```bash +./Build/Benchmarks/LuaBridgeBenchmarks +# optional iterations +./Build/Benchmarks/LuaBridgeBenchmarks 5000000 +``` + +The executable prints: +- `ns/op` (lower is better) +- `Mop/s` (higher is better) + +## Optional sol2 Side-by-Side + +To compare with sol2 in the same executable, enable the option below. The build will download sol2 through CMake FetchContent: + +```bash +cmake -S . -B Build \ + -DCMAKE_BUILD_TYPE=Release \ + -DLUABRIDGE_BENCHMARKS=ON \ + -DLUABRIDGE_BENCHMARK_WITH_SOL2=ON +cmake --build Build --target LuaBridgeBenchmarks --config Release +``` + +By default, FetchContent uses: +- Repository: `https://github.com/ThePhD/sol2.git` +- Tag: `v3.3.0` + +You can override these if needed: + +```bash +cmake -S . -B Build \ + -DCMAKE_BUILD_TYPE=Release \ + -DLUABRIDGE_BENCHMARKS=ON \ + -DLUABRIDGE_BENCHMARK_WITH_SOL2=ON \ + -DLUABRIDGE_SOL2_GIT_REPOSITORY=https://github.com/ThePhD/sol2.git \ + -DLUABRIDGE_SOL2_GIT_TAG=v3.3.0 +``` + +When enabled, the benchmark prints LuaBridge and sol2 rows for the same cases and iteration count. + +## Notes for Fair Comparison + +- Use Release mode and the same compiler for all libraries. +- Pin CPU frequency/governor where possible and avoid background load. +- Run each benchmark multiple times and compare medians. +- Keep Lua version identical between LuaBridge and sol2 runs. diff --git a/Benchmarks/benchmark_sol2_compare.cpp b/Benchmarks/benchmark_sol2_compare.cpp new file mode 100644 index 00000000..34a20a34 --- /dev/null +++ b/Benchmarks/benchmark_sol2_compare.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: MIT + +#include "Lua/LuaLibrary.h" +#include "LuaBridge/LuaBridge.h" + +#include +#include +#include +#include +#include +#include + +#if LUABRIDGE_BENCHMARK_WITH_SOL2 +#include +#endif + +namespace { + +struct Counter +{ + int value = 0; + + void inc() + { + ++value; + } + + int add(int x) + { + value += x; + return value; + } + + int get() const + { + return value; + } + + void set(int v) + { + value = v; + } +}; + +using Clock = std::chrono::steady_clock; + +struct CaseResult +{ + std::string name; + double nsPerOp = 0.0; + double mops = 0.0; +}; + +template +CaseResult runCase(const std::string& name, std::int64_t iterations, Fn&& fn) +{ + fn(); // warm-up + + const auto start = Clock::now(); + fn(); + const auto end = Clock::now(); + + const auto elapsedNs = std::chrono::duration_cast(end - start).count(); + const double nsPerOp = static_cast(elapsedNs) / static_cast(iterations); + const double mops = 1'000.0 / nsPerOp; + + return { name, nsPerOp, mops }; +} + +void printResult(const char* family, const CaseResult& r) +{ + std::cout << std::left << std::setw(12) << family + << " " << std::setw(30) << r.name + << " " << std::fixed << std::setprecision(2) + << std::setw(12) << r.nsPerOp + << " " << std::setw(10) << r.mops + << '\n'; +} + +void runLuaBridgeBenchmarks(std::int64_t iterations) +{ + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + luabridge::registerMainThread(L); + +#if LUABRIDGE_HAS_EXCEPTIONS + luabridge::enableExceptions(L); +#endif + + luabridge::getGlobalNamespace(L) + .beginClass("Counter") + .addConstructor() + .addFunction("inc", &Counter::inc) + .addFunction("add", &Counter::add) + .addProperty("value", &Counter::get, &Counter::set) + .endClass() + .addFunction("add", +[](int a, int b) { return a + b; }); + + const auto doLua = [L](const char* chunk) + { + if (luaL_dostring(L, chunk) != LUABRIDGE_LUA_OK) + { + const char* message = lua_tostring(L, -1); + std::cerr << "Lua error: " << (message ? message : "unknown") << '\n'; + std::exit(1); + } + }; + + doLua("obj = Counter()"); + + const std::string emptyLoop = "for i = 1, " + std::to_string(iterations) + " do end"; + const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; + const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; + const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; + + const auto empty = runCase("lua_empty_loop", iterations, [&] { doLua(emptyLoop.c_str()); }); + const auto freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { doLua(freeCallLoop.c_str()); }); + const auto memberCall = runCase("lua_to_cpp_member", iterations, [&] { doLua(memberCallLoop.c_str()); }); + const auto property = runCase("lua_to_cpp_property", iterations, [&] { doLua(propertyLoop.c_str()); }); + + CaseResult cppToLua; + { + doLua("function f(a, b) return a + b end"); + auto f = luabridge::getGlobal(L, "f"); + cppToLua = runCase("cpp_to_lua_call", iterations, [&] + { + for (std::int64_t i = 0; i < iterations; ++i) + (void) f(static_cast(i), static_cast(i)); + }); + } + + printResult("LuaBridge", empty); + printResult("LuaBridge", freeCall); + printResult("LuaBridge", memberCall); + printResult("LuaBridge", property); + printResult("LuaBridge", cppToLua); + + lua_close(L); +} + +#if LUABRIDGE_BENCHMARK_WITH_SOL2 +void runSol2Benchmarks(std::int64_t iterations) +{ + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::table, sol::lib::string); + + Counter counter; + + lua.new_usertype("Counter", + sol::call_constructor, + sol::constructors(), + "inc", &Counter::inc, + "add", &Counter::add, + "value", sol::property(&Counter::get, &Counter::set)); + + lua.set_function("add", +[](int a, int b) { return a + b; }); + lua["counter"] = &counter; + lua.script("obj = Counter()"); + + const std::string emptyLoop = "for i = 1, " + std::to_string(iterations) + " do end"; + const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; + const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; + const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; + + const auto empty = runCase("lua_empty_loop", iterations, [&] { lua.script(emptyLoop); }); + const auto freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { lua.script(freeCallLoop); }); + const auto memberCall = runCase("lua_to_cpp_member", iterations, [&] { lua.script(memberCallLoop); }); + const auto property = runCase("lua_to_cpp_property", iterations, [&] { lua.script(propertyLoop); }); + + lua.script("function f(a, b) return a + b end"); + sol::function f = lua["f"]; + const auto cppToLua = runCase("cpp_to_lua_call", iterations, [&] + { + for (std::int64_t i = 0; i < iterations; ++i) + (void) f(static_cast(i), static_cast(i)); + }); + + printResult("sol2", empty); + printResult("sol2", freeCall); + printResult("sol2", memberCall); + printResult("sol2", property); + printResult("sol2", cppToLua); +} +#endif + +} // namespace + +int main(int argc, char** argv) +{ + std::int64_t iterations = 2'000'000; + + if (argc > 1) + { + const auto parsed = std::strtoll(argv[1], nullptr, 10); + if (parsed > 0) + iterations = parsed; + } + + std::cout << "Iterations: " << iterations << '\n'; + std::cout << std::left << std::setw(12) << "Family" + << " " << std::setw(30) << "Case" + << " " << std::setw(12) << "ns/op" + << " " << std::setw(10) << "Mop/s" + << '\n'; + + runLuaBridgeBenchmarks(iterations); + +#if LUABRIDGE_BENCHMARK_WITH_SOL2 + runSol2Benchmarks(iterations); +#else + std::cout << "sol2 not enabled (set LUABRIDGE_BENCHMARK_WITH_SOL2=ON and SOL2_INCLUDE_DIR)." << '\n'; +#endif + + return 0; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 722058f9..ccee8949 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ find_program (GENHTML_EXECUTABLE genhtml) cmake_dependent_option (LUABRIDGE_TESTING "Build tests" ON "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) cmake_dependent_option (LUABRIDGE_COVERAGE "Enable coverage" OFF "LUABRIDGE_TESTING;FIND_EXECUTABLE;LCOV_EXECUTABLE;GENHTML_EXECUTABLE" OFF) +cmake_dependent_option (LUABRIDGE_BENCHMARKS "Build benchmark executable" OFF "CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR" OFF) add_subdirectory (Source) @@ -25,6 +26,10 @@ if (LUABRIDGE_TESTING) add_subdirectory (Tests) endif () +if (LUABRIDGE_BENCHMARKS) + add_subdirectory (Benchmarks) +endif () + add_custom_target (Documentation SOURCES CHANGES.md README.md diff --git a/Source/LuaBridge/detail/LuaHelpers.h b/Source/LuaBridge/detail/LuaHelpers.h index 0be232d7..c93f1c25 100644 --- a/Source/LuaBridge/detail/LuaHelpers.h +++ b/Source/LuaBridge/detail/LuaHelpers.h @@ -88,10 +88,7 @@ inline int get_length(lua_State* L, int idx) #else // LUA_VERSION_NUM >= 502 inline int get_length(lua_State* L, int idx) { - lua_len(L, idx); - const int len = static_cast(luaL_checknumber(L, -1)); - lua_pop(L, 1); - return len; + return static_cast(lua_rawlen(L, idx)); } #endif // LUA_VERSION_NUM < 502 diff --git a/Source/LuaBridge/detail/Stack.h b/Source/LuaBridge/detail/Stack.h index 3e354315..51df9554 100644 --- a/Source/LuaBridge/detail/Stack.h +++ b/Source/LuaBridge/detail/Stack.h @@ -261,8 +261,8 @@ struct Stack { if (lua_type(L, index) == LUA_TSTRING) { - std::size_t len; - luaL_checklstring(L, index, &len); + std::size_t len = 0; + lua_tolstring(L, index, &len); return len == 1; } From be51c00a31fe2511e9f9363227f5f385efa8a8d4 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 16:38:10 +0200 Subject: [PATCH 02/10] More improvements to callables --- Benchmarks/Benchmark.cpp | 388 ++++++++++++++++++++++++++ Benchmarks/CMakeLists.txt | 49 ++-- Benchmarks/benchmark_sol2_compare.cpp | 215 -------------- Source/LuaBridge/detail/CFunctions.h | 189 ++++++++++++- Source/LuaBridge/detail/ClassInfo.h | 9 + Source/LuaBridge/detail/Invoke.h | 308 ++++++++++---------- Source/LuaBridge/detail/LuaRef.h | 37 ++- Source/LuaBridge/detail/Namespace.h | 82 +++++- Source/LuaBridge/detail/Result.h | 56 ++++ Source/LuaBridge/detail/Userdata.h | 23 +- Tests/Source/ExceptionTests.cpp | 2 +- Tests/Source/IssueTests.cpp | 22 +- Tests/Source/ListTests.cpp | 4 +- Tests/Source/LuaRefTests.cpp | 25 +- Tests/Source/MapTests.cpp | 4 +- Tests/Source/OptionalTests.cpp | 8 +- Tests/Source/PairTests.cpp | 2 +- Tests/Source/SetTests.cpp | 4 +- Tests/Source/Tests.cpp | 47 ++-- Tests/Source/UnorderedMapTests.cpp | 4 +- justfile | 5 + 21 files changed, 984 insertions(+), 499 deletions(-) create mode 100644 Benchmarks/Benchmark.cpp delete mode 100644 Benchmarks/benchmark_sol2_compare.cpp diff --git a/Benchmarks/Benchmark.cpp b/Benchmarks/Benchmark.cpp new file mode 100644 index 00000000..bfd7cd17 --- /dev/null +++ b/Benchmarks/Benchmark.cpp @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: MIT + +#include "Lua/LuaLibrary.h" +#include "LuaBridge/LuaBridge.h" + +#include +#include +#include +#include +#include +#include + +#if LUABRIDGE_BENCHMARK_WITH_SOL2 +#include +#endif + +namespace { + +struct Counter +{ + int value = 0; + + void inc() + { + ++value; + } + + int add(int x) + { + value += x; + return value; + } + + int get() const + { + return value; + } + + void set(int v) + { + value = v; + } +}; + +using Clock = std::chrono::steady_clock; + +struct CaseResult +{ + std::string name; + double nsPerOp = 0.0; + double mops = 0.0; +}; + +template +CaseResult runCase(const std::string& name, std::int64_t iterations, Fn&& fn) +{ + fn(); // warm-up + + const auto start = Clock::now(); + fn(); + const auto end = Clock::now(); + + const auto elapsedNs = std::chrono::duration_cast(end - start).count(); + const double nsPerOp = static_cast(elapsedNs) / static_cast(iterations); + const double mops = 1'000.0 / nsPerOp; + + return { name, nsPerOp, mops }; +} + +void printResult(const char* family, const CaseResult& r) +{ + std::cout << std::left << std::setw(12) << family + << " " << std::setw(30) << r.name + << " " << std::fixed << std::setprecision(2) + << std::setw(12) << r.nsPerOp + << " " << std::setw(10) << r.mops + << '\n'; +} + +void runLuaBridgeBenchmarks(std::int64_t iterations) +{ + lua_State* L = luaL_newstate(); + luaL_openlibs(L); + luabridge::registerMainThread(L); + +#if LUABRIDGE_HAS_EXCEPTIONS + luabridge::enableExceptions(L); +#endif + + luabridge::getGlobalNamespace(L) + .beginClass("Counter") + .addConstructor() + .addFunction("inc", &Counter::inc) + .addFunction("add", &Counter::add) + .addProperty("value", &Counter::get, &Counter::set) + .endClass() + .addFunction("add", +[](int a, int b) { return a + b; }); + + const auto doLua = [L](const char* chunk) + { + if (luaL_dostring(L, chunk) != LUABRIDGE_LUA_OK) + { + const char* message = lua_tostring(L, -1); + std::cerr << "Lua error: " << (message ? message : "unknown") << '\n'; + std::exit(1); + } + }; + + CaseResult empty; + CaseResult freeCall; + CaseResult memberCall; + CaseResult property; + CaseResult cppGlobalGet; + CaseResult cppGlobalSet; + CaseResult cppTableGet; + CaseResult cppTableSet; + CaseResult cppChainedGet; + CaseResult cppChainedSet; + CaseResult cppToLua; + CaseResult cppCFunctionThroughLua; + + { + doLua("obj = Counter()"); + doLua("tbl = { value = 0 }"); + doLua("chain = { inner = { value = 0 } }"); + luabridge::setGlobal(L, 1.0, "gvalue"); + + volatile double sink = 0.0; + + const std::string emptyLoop = "for i = 1, " + std::to_string(iterations) + " do end"; + const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; + const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; + const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; + + empty = runCase("lua_empty_loop", iterations, [&] { doLua(emptyLoop.c_str()); }); + freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { doLua(freeCallLoop.c_str()); }); + memberCall = runCase("lua_to_cpp_member", iterations, [&] { doLua(memberCallLoop.c_str()); }); + property = runCase("lua_to_cpp_property", iterations, [&] { doLua(propertyLoop.c_str()); }); + + cppGlobalGet = runCase("cpp_table_global_get", iterations, [&] + { + double x = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + x += static_cast(luabridge::getGlobal(L, "gvalue")); + sink = x; + }); + + cppGlobalSet = runCase("cpp_table_global_set", iterations, [&] + { + double v = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + { + v += 1.0; + luabridge::setGlobal(L, v, "gvalue"); + } + sink = static_cast(luabridge::getGlobal(L, "gvalue")); + }); + + const auto tableRef = luabridge::getGlobal(L, "tbl"); + cppTableGet = runCase("cpp_table_get", iterations, [&] + { + double x = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + x += static_cast(tableRef["value"]); + sink = x; + }); + + cppTableSet = runCase("cpp_table_set", iterations, [&] + { + double v = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + { + v += 1.0; + tableRef["value"] = v; + } + sink = static_cast(tableRef["value"]); + }); + + const auto chainRef = luabridge::getGlobal(L, "chain"); + cppChainedGet = runCase("cpp_table_chained_get", iterations, [&] + { + double x = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + x += static_cast(chainRef["inner"]["value"]); + sink = x; + }); + + cppChainedSet = runCase("cpp_table_chained_set", iterations, [&] + { + double v = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + { + v += 1.0; + chainRef["inner"]["value"] = v; + } + sink = static_cast(chainRef["inner"]["value"]); + }); + + doLua("function f(a, b) return a + b end"); + auto f = luabridge::getGlobal(L, "f"); + auto callable = f.callable(); + cppToLua = runCase("cpp_to_lua_call", iterations, [&] + { + for (std::int64_t i = 0; i < iterations; ++i) + (void) callable(static_cast(i), static_cast(i)); + }); + + auto addFn = luabridge::getGlobal(L, "add"); + cppCFunctionThroughLua = runCase("cpp_c_function_through_lua", iterations, [&] + { + int x = 0; + for (std::int64_t i = 0; i < iterations; ++i) + x += addFn.call(static_cast(i), static_cast(i)).valueOr(0); + sink = static_cast(x); + }); + } + + printResult("LuaBridge", empty); + printResult("LuaBridge", freeCall); + printResult("LuaBridge", memberCall); + printResult("LuaBridge", property); + printResult("LuaBridge", cppGlobalGet); + printResult("LuaBridge", cppGlobalSet); + printResult("LuaBridge", cppTableGet); + printResult("LuaBridge", cppTableSet); + printResult("LuaBridge", cppChainedGet); + printResult("LuaBridge", cppChainedSet); + printResult("LuaBridge", cppToLua); + printResult("LuaBridge", cppCFunctionThroughLua); + + lua_close(L); +} + +#if LUABRIDGE_BENCHMARK_WITH_SOL2 +void runSol2Benchmarks(std::int64_t iterations) +{ + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::table, sol::lib::string); + + Counter counter; + + lua.new_usertype("Counter", + sol::call_constructor, + sol::constructors(), + "inc", &Counter::inc, + "add", &Counter::add, + "value", sol::property(&Counter::get, &Counter::set)); + + lua.set_function("add", +[](int a, int b) { return a + b; }); + lua["counter"] = &counter; + lua.script("obj = Counter()"); + lua.script("tbl = { value = 0 }"); + lua.script("chain = { inner = { value = 0 } }"); + lua["gvalue"] = 1.0; + + volatile double sink = 0.0; + + const std::string emptyLoop = "for i = 1, " + std::to_string(iterations) + " do end"; + const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; + const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; + const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; + + const auto empty = runCase("lua_empty_loop", iterations, [&] { lua.script(emptyLoop); }); + const auto freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { lua.script(freeCallLoop); }); + const auto memberCall = runCase("lua_to_cpp_member", iterations, [&] { lua.script(memberCallLoop); }); + const auto property = runCase("lua_to_cpp_property", iterations, [&] { lua.script(propertyLoop); }); + + const auto cppGlobalGet = runCase("cpp_table_global_get", iterations, [&] + { + double x = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + x += lua["gvalue"].get(); + sink = x; + }); + + const auto cppGlobalSet = runCase("cpp_table_global_set", iterations, [&] + { + double v = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + { + v += 1.0; + lua["gvalue"] = v; + } + sink = lua["gvalue"].get(); + }); + + sol::table table = lua["tbl"]; + const auto cppTableGet = runCase("cpp_table_get", iterations, [&] + { + double x = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + x += table["value"].get(); + sink = x; + }); + + const auto cppTableSet = runCase("cpp_table_set", iterations, [&] + { + double v = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + { + v += 1.0; + table["value"] = v; + } + sink = table["value"].get(); + }); + + sol::table chain = lua["chain"]; + const auto cppChainedGet = runCase("cpp_table_chained_get", iterations, [&] + { + double x = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + x += chain["inner"]["value"].get(); + sink = x; + }); + + const auto cppChainedSet = runCase("cpp_table_chained_set", iterations, [&] + { + double v = 0.0; + for (std::int64_t i = 0; i < iterations; ++i) + { + v += 1.0; + chain["inner"]["value"] = v; + } + sink = chain["inner"]["value"].get(); + }); + + lua.script("function f(a, b) return a + b end"); + sol::function f = lua["f"]; + const auto cppToLua = runCase("cpp_to_lua_call", iterations, [&] + { + for (std::int64_t i = 0; i < iterations; ++i) + (void) f(static_cast(i), static_cast(i)); + }); + + sol::function addFn = lua["add"]; + const auto cppCFunctionThroughLua = runCase("cpp_c_function_through_lua", iterations, [&] + { + int x = 0; + for (std::int64_t i = 0; i < iterations; ++i) + x += addFn.call(static_cast(i), static_cast(i)); + sink = static_cast(x); + }); + + printResult("sol2", empty); + printResult("sol2", freeCall); + printResult("sol2", memberCall); + printResult("sol2", property); + printResult("sol2", cppGlobalGet); + printResult("sol2", cppGlobalSet); + printResult("sol2", cppTableGet); + printResult("sol2", cppTableSet); + printResult("sol2", cppChainedGet); + printResult("sol2", cppChainedSet); + printResult("sol2", cppToLua); + printResult("sol2", cppCFunctionThroughLua); +} +#endif + +} // namespace + +int main(int argc, char** argv) +{ + std::int64_t iterations = 2'000'000; + + if (argc > 1) + { + const auto parsed = std::strtoll(argv[1], nullptr, 10); + if (parsed > 0) + iterations = parsed; + } + + std::cout << "Iterations: " << iterations << '\n'; + std::cout << std::left << std::setw(12) << "Family" + << " " << std::setw(30) << "Case" + << " " << std::setw(12) << "ns/op" + << " " << std::setw(10) << "Mop/s" + << '\n'; + + runLuaBridgeBenchmarks(iterations); + +#if LUABRIDGE_BENCHMARK_WITH_SOL2 + runSol2Benchmarks(iterations); +#else + std::cout << "sol2 not enabled (set LUABRIDGE_BENCHMARK_WITH_SOL2=ON and SOL2_INCLUDE_DIR)." << '\n'; +#endif + + return 0; +} diff --git a/Benchmarks/CMakeLists.txt b/Benchmarks/CMakeLists.txt index 0a06913d..5711a117 100644 --- a/Benchmarks/CMakeLists.txt +++ b/Benchmarks/CMakeLists.txt @@ -3,9 +3,8 @@ cmake_minimum_required(VERSION 3.10) include(FetchContent) set(LUABRIDGE_BENCHMARK_SOURCES - benchmark_sol2_compare.cpp - ../Tests/Lua/LuaLibrary5.4.8.cpp -) + Benchmark.cpp + ../Tests/Lua/LuaLibrary5.4.8.cpp) add_executable(LuaBridgeBenchmarks ${LUABRIDGE_BENCHMARK_SOURCES}) @@ -13,32 +12,24 @@ target_include_directories(LuaBridgeBenchmarks PRIVATE ${CMAKE_CURRENT_LIST_DIR}/.. ${CMAKE_CURRENT_LIST_DIR}/../Source ${CMAKE_CURRENT_LIST_DIR}/../Tests - ${CMAKE_CURRENT_LIST_DIR}/../Tests/Lua/Lua.5.4.8/src -) + ${CMAKE_CURRENT_LIST_DIR}/../Tests/Lua/Lua.5.4.8/src) target_compile_definitions(LuaBridgeBenchmarks PRIVATE LUABRIDGE_BENCHMARK_LUA54=1 - LUABRIDGE_TEST_LUA_VERSION=504 -) - -set(LUABRIDGE_BENCHMARK_WITH_SOL2 OFF CACHE BOOL "Enable optional sol2 comparison in benchmark executable") - -if (LUABRIDGE_BENCHMARK_WITH_SOL2) - set(LUABRIDGE_SOL2_GIT_REPOSITORY "https://github.com/ThePhD/sol2.git" CACHE STRING "sol2 repository URL") - set(LUABRIDGE_SOL2_GIT_TAG "v3.3.0" CACHE STRING "sol2 git tag or commit") - - # sol2 is header-only; FetchContent gives us a reproducible version without manual setup. - FetchContent_Declare( - sol2 - GIT_REPOSITORY ${LUABRIDGE_SOL2_GIT_REPOSITORY} - GIT_TAG ${LUABRIDGE_SOL2_GIT_TAG} - ) - FetchContent_MakeAvailable(sol2) - - target_include_directories(LuaBridgeBenchmarks PRIVATE ${sol2_SOURCE_DIR}/include) - target_compile_definitions(LuaBridgeBenchmarks PRIVATE - LUABRIDGE_BENCHMARK_WITH_SOL2=1 - SOL_STD_OPTIONAL=1 - SOL_LUA_VERSION=504 - ) -endif () + LUABRIDGE_TEST_LUA_VERSION=504) + +set(LUABRIDGE_SOL2_GIT_REPOSITORY "https://github.com/ThePhD/sol2.git" CACHE STRING "sol2 repository URL") +set(LUABRIDGE_SOL2_GIT_TAG "v3.3.0" CACHE STRING "sol2 git tag or commit") + +FetchContent_Declare( + sol2 + GIT_REPOSITORY ${LUABRIDGE_SOL2_GIT_REPOSITORY} + GIT_TAG ${LUABRIDGE_SOL2_GIT_TAG}) +FetchContent_MakeAvailable(sol2) + +target_include_directories(LuaBridgeBenchmarks PRIVATE + ${sol2_SOURCE_DIR}/include) + +target_compile_definitions(LuaBridgeBenchmarks PRIVATE + LUABRIDGE_BENCHMARK_WITH_SOL2=1 + SOL_LUA_VERSION=504) diff --git a/Benchmarks/benchmark_sol2_compare.cpp b/Benchmarks/benchmark_sol2_compare.cpp deleted file mode 100644 index 34a20a34..00000000 --- a/Benchmarks/benchmark_sol2_compare.cpp +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include "Lua/LuaLibrary.h" -#include "LuaBridge/LuaBridge.h" - -#include -#include -#include -#include -#include -#include - -#if LUABRIDGE_BENCHMARK_WITH_SOL2 -#include -#endif - -namespace { - -struct Counter -{ - int value = 0; - - void inc() - { - ++value; - } - - int add(int x) - { - value += x; - return value; - } - - int get() const - { - return value; - } - - void set(int v) - { - value = v; - } -}; - -using Clock = std::chrono::steady_clock; - -struct CaseResult -{ - std::string name; - double nsPerOp = 0.0; - double mops = 0.0; -}; - -template -CaseResult runCase(const std::string& name, std::int64_t iterations, Fn&& fn) -{ - fn(); // warm-up - - const auto start = Clock::now(); - fn(); - const auto end = Clock::now(); - - const auto elapsedNs = std::chrono::duration_cast(end - start).count(); - const double nsPerOp = static_cast(elapsedNs) / static_cast(iterations); - const double mops = 1'000.0 / nsPerOp; - - return { name, nsPerOp, mops }; -} - -void printResult(const char* family, const CaseResult& r) -{ - std::cout << std::left << std::setw(12) << family - << " " << std::setw(30) << r.name - << " " << std::fixed << std::setprecision(2) - << std::setw(12) << r.nsPerOp - << " " << std::setw(10) << r.mops - << '\n'; -} - -void runLuaBridgeBenchmarks(std::int64_t iterations) -{ - lua_State* L = luaL_newstate(); - luaL_openlibs(L); - luabridge::registerMainThread(L); - -#if LUABRIDGE_HAS_EXCEPTIONS - luabridge::enableExceptions(L); -#endif - - luabridge::getGlobalNamespace(L) - .beginClass("Counter") - .addConstructor() - .addFunction("inc", &Counter::inc) - .addFunction("add", &Counter::add) - .addProperty("value", &Counter::get, &Counter::set) - .endClass() - .addFunction("add", +[](int a, int b) { return a + b; }); - - const auto doLua = [L](const char* chunk) - { - if (luaL_dostring(L, chunk) != LUABRIDGE_LUA_OK) - { - const char* message = lua_tostring(L, -1); - std::cerr << "Lua error: " << (message ? message : "unknown") << '\n'; - std::exit(1); - } - }; - - doLua("obj = Counter()"); - - const std::string emptyLoop = "for i = 1, " + std::to_string(iterations) + " do end"; - const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; - const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; - const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; - - const auto empty = runCase("lua_empty_loop", iterations, [&] { doLua(emptyLoop.c_str()); }); - const auto freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { doLua(freeCallLoop.c_str()); }); - const auto memberCall = runCase("lua_to_cpp_member", iterations, [&] { doLua(memberCallLoop.c_str()); }); - const auto property = runCase("lua_to_cpp_property", iterations, [&] { doLua(propertyLoop.c_str()); }); - - CaseResult cppToLua; - { - doLua("function f(a, b) return a + b end"); - auto f = luabridge::getGlobal(L, "f"); - cppToLua = runCase("cpp_to_lua_call", iterations, [&] - { - for (std::int64_t i = 0; i < iterations; ++i) - (void) f(static_cast(i), static_cast(i)); - }); - } - - printResult("LuaBridge", empty); - printResult("LuaBridge", freeCall); - printResult("LuaBridge", memberCall); - printResult("LuaBridge", property); - printResult("LuaBridge", cppToLua); - - lua_close(L); -} - -#if LUABRIDGE_BENCHMARK_WITH_SOL2 -void runSol2Benchmarks(std::int64_t iterations) -{ - sol::state lua; - lua.open_libraries(sol::lib::base, sol::lib::math, sol::lib::table, sol::lib::string); - - Counter counter; - - lua.new_usertype("Counter", - sol::call_constructor, - sol::constructors(), - "inc", &Counter::inc, - "add", &Counter::add, - "value", sol::property(&Counter::get, &Counter::set)); - - lua.set_function("add", +[](int a, int b) { return a + b; }); - lua["counter"] = &counter; - lua.script("obj = Counter()"); - - const std::string emptyLoop = "for i = 1, " + std::to_string(iterations) + " do end"; - const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; - const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; - const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; - - const auto empty = runCase("lua_empty_loop", iterations, [&] { lua.script(emptyLoop); }); - const auto freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { lua.script(freeCallLoop); }); - const auto memberCall = runCase("lua_to_cpp_member", iterations, [&] { lua.script(memberCallLoop); }); - const auto property = runCase("lua_to_cpp_property", iterations, [&] { lua.script(propertyLoop); }); - - lua.script("function f(a, b) return a + b end"); - sol::function f = lua["f"]; - const auto cppToLua = runCase("cpp_to_lua_call", iterations, [&] - { - for (std::int64_t i = 0; i < iterations; ++i) - (void) f(static_cast(i), static_cast(i)); - }); - - printResult("sol2", empty); - printResult("sol2", freeCall); - printResult("sol2", memberCall); - printResult("sol2", property); - printResult("sol2", cppToLua); -} -#endif - -} // namespace - -int main(int argc, char** argv) -{ - std::int64_t iterations = 2'000'000; - - if (argc > 1) - { - const auto parsed = std::strtoll(argv[1], nullptr, 10); - if (parsed > 0) - iterations = parsed; - } - - std::cout << "Iterations: " << iterations << '\n'; - std::cout << std::left << std::setw(12) << "Family" - << " " << std::setw(30) << "Case" - << " " << std::setw(12) << "ns/op" - << " " << std::setw(10) << "Mop/s" - << '\n'; - - runLuaBridgeBenchmarks(iterations); - -#if LUABRIDGE_BENCHMARK_WITH_SOL2 - runSol2Benchmarks(iterations); -#else - std::cout << "sol2 not enabled (set LUABRIDGE_BENCHMARK_WITH_SOL2=ON and SOL2_INCLUDE_DIR)." << '\n'; -#endif - - return 0; -} diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index 9cdc95b4..3ba3f295 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -428,6 +428,106 @@ inline int index_metamethod(lua_State* L) // no return } +template +inline int index_metamethod_simple(lua_State* L) +{ +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(L, 3, detail::error_lua_stack_overflow); +#endif + + LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); + + lua_getmetatable(L, 1); // Stack: mt + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + const char* key = lua_tostring(L, 2); + + const auto rawlookup = [L, key]() + { + if (key != nullptr) + rawgetfield(L, -1, key); + else + { + lua_pushvalue(L, 2); + lua_rawget(L, -2); + } + }; + + // For userdata instance property access, checking propget first avoids an always-miss lookup + // in the class table for common property keys. + if (lua_isuserdata(L, 1)) + { + lua_rawgetp_x(L, -1, getPropgetKey()); // Stack: mt, pg + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + rawlookup(); // Stack: mt, pg, getter | nil + lua_remove(L, -2); // Stack: mt, getter | nil + + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); // Stack: getter + lua_pushvalue(L, 1); // Stack: getter, self + lua_call(L, 1, 1); // Stack: value + return 1; + } + + lua_pop(L, 1); // Stack: mt + } + if (key != nullptr && is_metamethod(key)) + { + lua_pushnil(L); + return 1; + } + + if (lua_istable(L, 1)) + { + if constexpr (IsObject) + lua_pushvalue(L, 1); // Stack: mt, self + else + push_class_or_const_table(L, -1); // Stack: mt, cl | co + + if (lua_istable(L, -1)) + { + rawlookup(); // Stack: mt, self | cl | co, value | nil + lua_remove(L, -2); // Stack: mt, value | nil + if (! lua_isnil(L, -1)) + { + lua_remove(L, -2); // Stack: value + return 1; + } + } + + lua_pop(L, 1); // Stack: mt + } + + rawlookup(); // Stack: mt, value | nil + if (! lua_isnil(L, -1)) + { + lua_remove(L, -2); // Stack: value + return 1; + } + + lua_pop(L, 1); // Stack: mt + + lua_rawgetp_x(L, -1, getPropgetKey()); // Stack: mt, pg + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + rawlookup(); // Stack: mt, pg, getter | nil + lua_remove(L, -2); // Stack: mt, getter | nil + + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); // Stack: getter + lua_pushvalue(L, 1); // Stack: getter, self + lua_call(L, 1, 1); // Stack: value + return 1; + } + + lua_pop(L, 2); // Stack: - + lua_pushnil(L); + return 1; +} + //================================================================================================= /** * @brief __newindex metamethod for non-static members. @@ -684,6 +784,47 @@ inline int newindex_metamethod(lua_State* L) return 0; } +template +inline int newindex_metamethod_simple(lua_State* L) +{ +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(L, 3, detail::error_lua_stack_overflow); +#endif + + LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); + + lua_getmetatable(L, 1); // Stack: mt + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + const char* key = lua_tostring(L, 2); + + lua_rawgetp_x(L, -1, getPropsetKey()); // Stack: mt, ps | nil + if (! lua_istable(L, -1)) + luaL_error(L, "no member named '%s'", key); + + if (key != nullptr) + rawgetfield(L, -1, key); // Stack: mt, ps, setter | nil + else + { + lua_pushvalue(L, 2); // Stack: mt, ps, key + lua_rawget(L, -2); // Stack: mt, ps, setter | nil + } + lua_remove(L, -2); // Stack: mt, setter | nil + + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); // Stack: setter + if constexpr (IsObject) + lua_pushvalue(L, 1); // Stack: setter, self + lua_pushvalue(L, 3); // Stack: setter, self, value + lua_call(L, IsObject ? 2 : 1, 0); + return 0; + } + + luaL_error(L, "no writable member '%s'", key); + return 0; +} + //================================================================================================= /** * @brief lua_CFunction to report an error writing to a read-only value. @@ -939,6 +1080,42 @@ inline void add_property_setter(lua_State* L, const char* name, int tableIndex) /** * @brief Function generator. */ +template +decltype(auto) invoke_callable_from_stack_impl(lua_State* L, F&& func, std::index_sequence) +{ + return std::invoke( + std::forward(func), + unwrap_argument_or_error>(L, Indices, Start)...); +} + +template +decltype(auto) invoke_callable_from_stack(lua_State* L, F&& func) +{ + return invoke_callable_from_stack_impl( + L, + std::forward(func), + std::make_index_sequence>()); +} + +template +decltype(auto) invoke_member_callable_from_stack_impl(lua_State* L, T* ptr, F&& func, std::index_sequence) +{ + return std::invoke( + std::forward(func), + ptr, + unwrap_argument_or_error>(L, Indices, Start)...); +} + +template +decltype(auto) invoke_member_callable_from_stack(lua_State* L, T* ptr, F&& func) +{ + return invoke_member_callable_from_stack_impl( + L, + ptr, + std::forward(func), + std::make_index_sequence>()); +} + template struct function { @@ -951,7 +1128,7 @@ struct function try { #endif - result = Stack::push(L, std::apply(std::forward(func), make_arguments_list(L))); + result = Stack::push(L, invoke_callable_from_stack(L, std::forward(func))); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -976,9 +1153,7 @@ struct function try { #endif - auto f = [ptr, func = std::forward(func)](auto&&... args) -> ReturnType { return (ptr->*func)(std::forward(args)...); }; - - result = Stack::push(L, std::apply(f, make_arguments_list(L))); + result = Stack::push(L, invoke_member_callable_from_stack(L, ptr, std::forward(func))); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -1005,7 +1180,7 @@ struct function try { #endif - std::apply(std::forward(func), make_arguments_list(L)); + invoke_callable_from_stack(L, std::forward(func)); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -1025,9 +1200,7 @@ struct function try { #endif - auto f = [ptr, func = std::forward(func)](auto&&... args) { (ptr->*func)(std::forward(args)...); }; - - std::apply(f, make_arguments_list(L)); + invoke_member_callable_from_stack(L, ptr, std::forward(func)); #if LUABRIDGE_HAS_EXCEPTIONS } diff --git a/Source/LuaBridge/detail/ClassInfo.h b/Source/LuaBridge/detail/ClassInfo.h index b1ee1aa8..9de1308c 100644 --- a/Source/LuaBridge/detail/ClassInfo.h +++ b/Source/LuaBridge/detail/ClassInfo.h @@ -101,6 +101,15 @@ template ().find_first_of('.')> return reinterpret_cast(0xc2b); } +//================================================================================================= +/** + * @brief The key of a type identity tag in class/const metatables. + */ +[[nodiscard]] inline const void* getTypeIdentityKey() noexcept +{ + return reinterpret_cast(0xc2c); +} + //================================================================================================= /** * @brief The key of a propget table in another metatable. diff --git a/Source/LuaBridge/detail/Invoke.h b/Source/LuaBridge/detail/Invoke.h index 34886195..71645c4d 100644 --- a/Source/LuaBridge/detail/Invoke.h +++ b/Source/LuaBridge/detail/Invoke.h @@ -10,190 +10,111 @@ #include "LuaRef.h" #include "LuaException.h" -#include #include -#include -#include #include +#include +#include namespace luabridge { //================================================================================================= -/** - * @brief Result of a lua invocation. - */ -class LuaResult -{ -public: - /** - * @brief Get if the result was ok and didn't raise a lua error. - */ - explicit operator bool() const noexcept - { - return !m_ec; - } - - /** - * @brief Return if the invocation was ok and didn't raise a lua error. - */ - bool wasOk() const noexcept - { - return !m_ec; - } - - /** - * @brief Return if the invocation did raise a lua error. - */ - bool hasFailed() const noexcept - { - return !!m_ec; - } +namespace detail { - /** - * @brief Return the error code, if any. - * - * In case the invcation didn't raise any lua error, the value returned equals to a - * default constructed std::error_code. - */ - std::error_code errorCode() const noexcept - { - return m_ec; - } - - /** - * @brief Return the error message, if any. - */ - std::string errorMessage() const noexcept - { - if (std::holds_alternative(m_data)) - { - const auto& message = std::get(m_data); - return message.empty() ? m_ec.message() : message; - } - - return {}; - } - - /** - * @brief Return the number of return values. - */ - std::size_t size() const noexcept - { - if (std::holds_alternative>(m_data)) - return std::get>(m_data).size(); +template +struct IsTuple : std::false_type +{ +}; - return 0; - } +template +struct IsTuple> : std::true_type +{ +}; - /** - * @brief Get a return value at a specific index. - */ - LuaRef operator[](std::size_t index) const - { - LUABRIDGE_ASSERT(m_ec == std::error_code()); +template +TypeResult decodeTupleResult(lua_State* L, int firstResultIndex, std::index_sequence) +{ + Tuple value; + std::error_code ec; - if (std::holds_alternative>(m_data)) + const bool ok = + (([&]() { - const auto& values = std::get>(m_data); + using ElementType = std::tuple_element_t; - LUABRIDGE_ASSERT(index < values.size()); - return values[index]; - } + auto element = Stack::get(L, firstResultIndex + static_cast(Indices)); + if (! element) + { + ec = element.error(); + return false; + } - return LuaRef(m_L); - } + std::get(value) = std::move(*element); + return true; + }()) + && ...); -private: - template - friend LuaResult call(const LuaRef&, Args&&...); + if (! ok) + return ec; - template - friend LuaResult callWithHandler(const LuaRef&, F&&, Args&&...); + return value; +} - static LuaResult errorFromStack(lua_State* L, std::error_code ec) +template +TypeResult decodeCallResult(lua_State* L, int firstResultIndex, int numReturnedValues) +{ + if constexpr (std::is_same_v || std::is_same_v>) { - auto errorString = lua_tostring(L, -1); - lua_pop(L, 1); + if (numReturnedValues != 0) + return makeErrorCode(ErrorCode::InvalidTableSizeInCast); - return LuaResult(L, ec, errorString ? errorString : ec.message()); + return {}; } - - static LuaResult valuesFromStack(lua_State* L, int stackTop) + else + if constexpr (IsTuple::value) { - std::vector values; - - const int numReturnedValues = lua_gettop(L) - stackTop; - if (numReturnedValues > 0) - { - values.reserve(numReturnedValues); - - for (int index = numReturnedValues; index > 0; --index) - values.emplace_back(LuaRef::fromStack(L, -index)); - - lua_pop(L, numReturnedValues); - } + constexpr auto expectedSize = static_cast(std::tuple_size_v); + if (numReturnedValues != expectedSize) + return makeErrorCode(ErrorCode::InvalidTableSizeInCast); - return LuaResult(L, std::move(values)); + return decodeTupleResult(L, firstResultIndex, std::make_index_sequence>{}); } - - LuaResult(lua_State* L, std::error_code ec, std::string_view errorString) - : m_L(L) - , m_ec(ec) - , m_data(std::string(errorString)) + else { - } + if (numReturnedValues < 1) + return makeErrorCode(ErrorCode::InvalidTypeCast); - explicit LuaResult(lua_State* L, std::vector values) noexcept - : m_L(L) - , m_data(std::move(values)) - { + return Stack::get(L, firstResultIndex); } +} - lua_State* m_L = nullptr; - std::error_code m_ec; - std::variant, std::string> m_data; -}; +} // namespace detail //================================================================================================= /** - * @brief Safely call Lua code. - * - * These overloads allow Lua code to be called throught `lua_pcall`. The return value is provided as - * a `LuaResult` which will hold the return values or an error if the call failed. - * - * If an error occurs, a `LuaException` is thrown or if exceptions are disabled the function result will - * contain a error code and evaluate false. - * - * @note The function might throw a `LuaException` if the application is compiled with exceptions on - * and they are enabled globally by calling `enableExceptions` in two cases: - * - one of the arguments passed cannot be pushed in the stack, for example a unregistered C++ class - * - the lua invaction calls the panic handler, which is converted to a C++ exception - * - * @return A result object. -*/ -template -LuaResult callWithHandler(const LuaRef& object, F&& errorHandler, Args&&... args) + * @brief Safely call Lua code and decode the return values to R. + */ +template +TypeResult callWithHandler(const Ref& object, F&& errorHandler, Args&&... args) { static constexpr bool isValidHandler = !std::is_same_v, detail::remove_cvref_t>; lua_State* L = object.state(); - const int stackTop = lua_gettop(L); + const StackRestore stackRestore(L); + const int initialTop = lua_gettop(L); if constexpr (isValidHandler) - detail::push_function(L, std::forward(errorHandler), ""); // Stack: error handler (eh) + detail::push_function(L, std::forward(errorHandler), ""); - object.push(); // Stack: eh, ref + object.push(); { - const auto [result, index] = detail::push_arguments(L, std::forward_as_tuple(args...)); // Stack: eh, ref, args... + const auto [result, index] = detail::push_arguments(L, std::forward_as_tuple(args...)); if (! result) - { - lua_pop(L, static_cast(index) + 1); - return LuaResult(L, result, result.message()); - } + return result.error(); } - const int code = lua_pcall(L, sizeof...(Args), LUA_MULTRET, isValidHandler ? (-static_cast(sizeof...(Args)) - 2) : 0); + const int messageHandlerIndex = isValidHandler ? (initialTop + 1) : 0; + const int code = lua_pcall(L, sizeof...(Args), LUA_MULTRET, messageHandlerIndex); if (code != LUABRIDGE_LUA_OK) { auto ec = makeErrorCode(ErrorCode::LuaFunctionCallFailed); @@ -206,18 +127,79 @@ LuaResult callWithHandler(const LuaRef& object, F&& errorHandler, Args&&... args } #endif - return LuaResult::errorFromStack(L, ec); + lua_pop(L, 1); + return ec; } - return LuaResult::valuesFromStack(L, stackTop); + if constexpr (isValidHandler) + lua_remove(L, initialTop + 1); + + const int firstResultIndex = initialTop + 1; + const int numReturnedValues = lua_gettop(L) - initialTop; + return detail::decodeCallResult(L, firstResultIndex, numReturnedValues); } -template -LuaResult call(const LuaRef& object, Args&&... args) +template +TypeResult callWithHandler(const Ref& object, F&& errorHandler, Args&&... args) +{ + return callWithHandler(object, std::forward(errorHandler), std::forward(args)...); +} + +template +TypeResult call(const Ref& object, Args&&... args) { - return callWithHandler(object, std::ignore, std::forward(args)...); + return callWithHandler(object, std::ignore, std::forward(args)...); } +template +class LuaFunction; + +template +class LuaFunction +{ +public: + LuaFunction() = default; + + explicit LuaFunction(const LuaRef& function) + : m_function(function) + { + } + + explicit LuaFunction(LuaRef&& function) + : m_function(std::move(function)) + { + } + + [[nodiscard]] TypeResult operator()(Args... args) const + { + return call(std::forward(args)...); + } + + [[nodiscard]] TypeResult call(Args... args) const + { + return luabridge::call(m_function, std::forward(args)...); + } + + template + [[nodiscard]] TypeResult callWithHandler(F&& errorHandler, Args... args) const + { + return luabridge::callWithHandler(m_function, std::forward(errorHandler), std::forward(args)...); + } + + [[nodiscard]] bool isValid() const + { + return m_function.isCallable(); + } + + [[nodiscard]] const LuaRef& ref() const + { + return m_function; + } + +private: + LuaRef m_function; +}; + //============================================================================================= /** * @brief Wrapper for `lua_pcall` that throws if exceptions are enabled. @@ -236,24 +218,40 @@ inline int pcall(lua_State* L, int nargs = 0, int nresults = 0, int msgh = 0) //============================================================================================= template -template -LuaResult LuaRefBase::operator()(Args&&... args) const +template +TypeResult LuaRefBase::call(Args&&... args) const { - return luabridge::call(*this, std::forward(args)...); + return luabridge::call(impl(), std::forward(args)...); } template template -LuaResult LuaRefBase::call(Args&&... args) const +TypeResult LuaRefBase::operator()(Args&&... args) const +{ + return call(std::forward(args)...); +} + +template +template +TypeResult LuaRefBase::callWithHandler(F&& errorHandler, Args&&... args) const { - return luabridge::call(*this, std::forward(args)...); + return luabridge::callWithHandler(impl(), std::forward(errorHandler), std::forward(args)...); } template template -LuaResult LuaRefBase::callWithHandler(F&& errorHandler, Args&&... args) const +TypeResult LuaRefBase::callWithHandler(F&& errorHandler, Args&&... args) const +{ + return callWithHandler(std::forward(errorHandler), std::forward(args)...); +} + +template +template +LuaFunction LuaRefBase::callable() const { - return luabridge::callWithHandler(*this, std::forward(errorHandler), std::forward(args)...); + const StackRestore stackRestore(m_L); + impl().push(m_L); + return LuaFunction(LuaRef::fromStack(m_L)); } } // namespace luabridge diff --git a/Source/LuaBridge/detail/LuaRef.h b/Source/LuaBridge/detail/LuaRef.h index b8c400ad..1edc860a 100644 --- a/Source/LuaBridge/detail/LuaRef.h +++ b/Source/LuaBridge/detail/LuaRef.h @@ -23,7 +23,8 @@ namespace luabridge { -class LuaResult; +template +class LuaFunction; //================================================================================================= /** @@ -619,22 +620,36 @@ class LuaRefBase //============================================================================================= /** - * @brief Call Lua code. + * @brief Call Lua code and decode the first return value to R. * - * The return value is provided as a LuaRef (which may be LUA_REFNIL). - * - * If an error occurs, a LuaException is thrown (only if exceptions are enabled). - * - * @returns A result of the call. + * For tuple types, each tuple element is decoded from a distinct Lua return value. */ - template - LuaResult operator()(Args&&... args) const; + template + TypeResult call(Args&&... args) const; + /** + * @brief Call Lua code assuming no return values. + */ template - LuaResult call(Args&&... args) const; + TypeResult operator()(Args&&... args) const; + /** + * @brief Call Lua code with an explicit error handler and decode the return type. + */ + template + TypeResult callWithHandler(F&& errorHandler, Args&&... args) const; + + /** + * @brief Call Lua code with an explicit error handler and no return values. + */ template - LuaResult callWithHandler(F&& errorHandler, Args&&... args) const; + TypeResult callWithHandler(F&& errorHandler, Args&&... args) const; + + /** + * @brief Build a typed callable wrapper from this Lua object. + */ + template + LuaFunction callable() const; protected: lua_State* m_L = nullptr; diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 3932710c..603fe25b 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -159,6 +159,50 @@ class Namespace : public detail::Registrar using Registrar::operator=; protected: + void setObjectMetaMethods(int tableIndex, bool simple) + { + tableIndex = lua_absindex(L, tableIndex); + + if (simple) + { + lua_pushcfunction_x(L, &detail::index_metamethod_simple, "__index"); + rawsetfield(L, tableIndex, "__index"); + + lua_pushcfunction_x(L, &detail::newindex_metamethod_simple, "__newindex"); + rawsetfield(L, tableIndex, "__newindex"); + } + else + { + lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); + rawsetfield(L, tableIndex, "__index"); + + lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); + rawsetfield(L, tableIndex, "__newindex"); + } + } + + void setStaticMetaMethods(int tableIndex, bool simple) + { + tableIndex = lua_absindex(L, tableIndex); + + if (simple) + { + lua_pushcfunction_x(L, &detail::index_metamethod_simple, "__index"); + rawsetfield(L, tableIndex, "__index"); + + lua_pushcfunction_x(L, &detail::newindex_metamethod_simple, "__newindex"); + rawsetfield(L, tableIndex, "__newindex"); + } + else + { + lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); + rawsetfield(L, tableIndex, "__index"); + + lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); + rawsetfield(L, tableIndex, "__newindex"); + } + } + //========================================================================================= /** * @brief Create the const table. @@ -181,11 +225,7 @@ class Namespace : public detail::Registrar lua_pushstring(L, type_name.c_str()); // Stack: ns, co, name lua_rawsetp_x(L, -2, detail::getTypeKey()); // co [typeKey] = name. Stack: ns, co - lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); // Stack: ns, co, im - rawsetfield(L, -2, "__index"); // Stack: ns, co - - lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); // Stack: ns, co, nim - rawsetfield(L, -2, "__newindex"); // Stack: ns, co + setObjectMetaMethods(-1, ! options.test(extensibleClass)); // Stack: ns, co lua_newtable(L); // Stack: ns, co, tb lua_rawsetp_x(L, -2, detail::getPropgetKey()); // Stack: ns, co @@ -242,11 +282,7 @@ class Namespace : public detail::Registrar pushunsigned(L, options.toUnderlying()); // Stack: ns, co, cl, st, mt, options lua_rawsetp_x(L, -2, detail::getClassOptionsKey()); // st [classOptionsKey] = options. Stack: ns, co, cl, st, mt - lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); - rawsetfield(L, -2, "__index"); - - lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); - rawsetfield(L, -2, "__newindex"); + setStaticMetaMethods(-1, ! options.test(extensibleClass)); lua_newtable(L); // Stack: ns, co, cl, st, proget table (pg) lua_rawsetp_x(L, -2, detail::getPropgetKey()); // st [propgetKey] = pg. Stack: ns, co, cl, st @@ -326,6 +362,12 @@ class Namespace : public detail::Registrar createClassTable(name, options); // Stack: ns, co, class table (cl) ++m_stackSize; + + lua_pushlightuserdata(L, const_cast(detail::getConstRegistryKey())); // Stack: ns, co, cl, id + lua_rawsetp_x(L, -3, detail::getTypeIdentityKey()); // co[typeIdentityKey] = const id. Stack: ns, co, cl + lua_pushlightuserdata(L, const_cast(detail::getClassRegistryKey())); // Stack: ns, co, cl, id + lua_rawsetp_x(L, -2, detail::getTypeIdentityKey()); // cl[typeIdentityKey] = class id. Stack: ns, co, cl + #if !defined(LUABRIDGE_ON_LUAU) lua_pushcfunction_x(L, &detail::gc_metamethod, "__gc"); // Stack: ns, co, cl, function rawsetfield(L, -2, "__gc"); // cl ["__gc"] = function. Stack: ns, co, cl @@ -397,6 +439,12 @@ class Namespace : public detail::Registrar createClassTable(name, options); // Stack: ns, co, class table (cl) ++m_stackSize; + + lua_pushlightuserdata(L, const_cast(detail::getConstRegistryKey())); // Stack: ns, co, cl, id + lua_rawsetp_x(L, -3, detail::getTypeIdentityKey()); // co[typeIdentityKey] = const id. Stack: ns, co, cl + lua_pushlightuserdata(L, const_cast(detail::getClassRegistryKey())); // Stack: ns, co, cl, id + lua_rawsetp_x(L, -2, detail::getTypeIdentityKey()); // cl[typeIdentityKey] = class id. Stack: ns, co, cl + #if !defined(LUABRIDGE_ON_LUAU) lua_pushcfunction_x(L, &detail::gc_metamethod, "__gc"); // Stack: ns, co, cl, function rawsetfield(L, -2, "__gc"); // cl ["__gc"] = function. Stack: ns, co, cl @@ -433,6 +481,10 @@ class Namespace : public detail::Registrar 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 + setObjectMetaMethods(-3, false); // co + setObjectMetaMethods(-2, false); // cl + setStaticMetaMethods(-1, false); // st + lua_pushvalue(L, -1); // Stack: ns, co, cl, st, st lua_rawsetp_x(L, LUA_REGISTRYINDEX, detail::getStaticRegistryKey()); // Stack: ns, co, cl, st lua_pushvalue(L, -2); // Stack: ns, co, cl, st, cl @@ -569,6 +621,7 @@ class Namespace : public detail::Registrar 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()); + setStaticMetaMethods(-1, false); return *this; } @@ -582,6 +635,7 @@ class Namespace : public detail::Registrar 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()); + setStaticMetaMethods(-1, false); return *this; } @@ -604,6 +658,7 @@ class Namespace : public detail::Registrar 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()); + setStaticMetaMethods(-1, false); return *this; } @@ -617,6 +672,7 @@ class Namespace : public detail::Registrar 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()); + setStaticMetaMethods(-1, false); return *this; } @@ -1063,6 +1119,7 @@ class Namespace : public detail::Registrar 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, -3, detail::getIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -1076,6 +1133,7 @@ class Namespace : public detail::Registrar 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, -3, detail::getIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -1089,6 +1147,7 @@ class Namespace : public detail::Registrar new (lua_newuserdata_x(L, sizeof(MemFnPtr))) MemFnPtr(idxf); lua_pushcclosure_x(L, &detail::invoke_member_function, "__index", 1); lua_rawsetp_x(L, -3, detail::getIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -1111,6 +1170,7 @@ class Namespace : public detail::Registrar 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, -3, detail::getNewIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -1124,6 +1184,7 @@ class Namespace : public detail::Registrar 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, -3, detail::getNewIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -1137,6 +1198,7 @@ class Namespace : public detail::Registrar new (lua_newuserdata_x(L, sizeof(MemFnPtr))) MemFnPtr(idxf); lua_pushcclosure_x(L, &detail::invoke_member_function, "__newindex", 1); lua_rawsetp_x(L, -3, detail::getNewIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } diff --git a/Source/LuaBridge/detail/Result.h b/Source/LuaBridge/detail/Result.h index 36fdde4b..750f5405 100644 --- a/Source/LuaBridge/detail/Result.h +++ b/Source/LuaBridge/detail/Result.h @@ -164,6 +164,62 @@ struct TypeResult Expected m_value; }; +template <> +struct TypeResult +{ + TypeResult() noexcept = default; + + TypeResult(std::error_code ec) noexcept + : m_ec(ec) + { + } + + TypeResult(const TypeResult&) noexcept = default; + TypeResult(TypeResult&&) noexcept = default; + TypeResult& operator=(const TypeResult&) noexcept = default; + TypeResult& operator=(TypeResult&&) noexcept = default; + + explicit operator bool() const noexcept + { + return ! m_ec; + } + + void value() const noexcept + { + } + + std::error_code error() const noexcept + { + return m_ec; + } + + const char* error_cstr() const noexcept + { + return detail::ErrorCategory::errorString(m_ec.value()); + } + + operator std::error_code() const noexcept + { + return m_ec; + } + + std::string message() const + { + return m_ec.message(); + } + +#if LUABRIDGE_HAS_EXCEPTIONS + void throw_on_error() const + { + if (m_ec) + throw std::system_error(m_ec); + } +#endif + +private: + std::error_code m_ec; +}; + template inline bool operator==(const TypeResult& lhs, const U& rhs) noexcept { diff --git a/Source/LuaBridge/detail/Userdata.h b/Source/LuaBridge/detail/Userdata.h index 7376011a..cd366cd8 100644 --- a/Source/LuaBridge/detail/Userdata.h +++ b/Source/LuaBridge/detail/Userdata.h @@ -238,7 +238,28 @@ class Userdata if (lua_isnil(L, index)) return nullptr; - auto* clazz = getClass(L, index, detail::getConstRegistryKey(), detail::getClassRegistryKey(), canBeConst); + const int absIndex = lua_absindex(L, index); + const auto classId = detail::getClassRegistryKey(); + const auto constId = detail::getConstRegistryKey(); + + // Common-case fast path: exact class/const metatable match. + // This avoids parent-chain traversal and registry table lookups. + if (lua_getmetatable(L, absIndex) && lua_istable(L, -1)) + { + lua_rawgetp_x(L, -1, detail::getTypeIdentityKey()); + const void* identity = lua_touserdata(L, -1); + lua_pop(L, 1); + + if (identity == classId || (canBeConst && identity == constId)) + { + lua_pop(L, 1); + return static_cast(static_cast(lua_touserdata(L, absIndex))->getPointer()); + } + + lua_pop(L, 1); + } + + auto* clazz = getClass(L, absIndex, constId, classId, canBeConst); if (! clazz) return nullptr; diff --git a/Tests/Source/ExceptionTests.cpp b/Tests/Source/ExceptionTests.cpp index 756c1c7d..aacce795 100644 --- a/Tests/Source/ExceptionTests.cpp +++ b/Tests/Source/ExceptionTests.cpp @@ -231,7 +231,7 @@ TEST_F(ExceptionTests, Bug153) try { - (*luaCallback)(); + luaCallback->call>(); } catch (const std::exception& e) { diff --git a/Tests/Source/IssueTests.cpp b/Tests/Source/IssueTests.cpp index 5fee479b..032fa252 100644 --- a/Tests/Source/IssueTests.cpp +++ b/Tests/Source/IssueTests.cpp @@ -179,23 +179,23 @@ TEST_F(IssueTests, Issue8) luabridge::LuaRef func = luabridge::getGlobal(L, "HelloWorld"); { - auto result = func("helloworld"); - ASSERT_EQ(1, result.size()); - ASSERT_STREQ("helloworld", result[0].unsafe_cast()); + auto result = func.call("helloworld"); + ASSERT_TRUE(result); + ASSERT_EQ("helloworld", *result); } { const char* str = "helloworld"; - auto result = func(str); - ASSERT_EQ(1, result.size()); - ASSERT_STREQ("helloworld", result[0].unsafe_cast()); + auto result = func.call(str); + ASSERT_TRUE(result); + ASSERT_EQ("helloworld", *result); } { std::string str = "helloworld"; - auto result = func(std::move(str)); - ASSERT_EQ(1, result.size()); - ASSERT_STREQ("helloworld", result[0].unsafe_cast()); + auto result = func.call(std::move(str)); + ASSERT_TRUE(result); + ASSERT_EQ("helloworld", *result); } } @@ -212,7 +212,7 @@ struct SomeClass void SomeMember() { if (override_.isFunction()) - override_(); + override_.call(); } }; } // namespace @@ -264,5 +264,5 @@ TEST_F(IssueTests, IssueMainThread) } luabridge::LuaRef test = luabridge::getGlobal(L, "test"); - test(); + EXPECT_TRUE(test.call()); } diff --git a/Tests/Source/ListTests.cpp b/Tests/Source/ListTests.cpp index afa75d3a..dd3d3cd5 100644 --- a/Tests/Source/ListTests.cpp +++ b/Tests/Source/ListTests.cpp @@ -81,14 +81,14 @@ TEST_F(ListTests, PassToFunction) resetResult(); std::list lvalue{ 10, 20, 30 }; - foo(lvalue); + ASSERT_TRUE(foo.call(lvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(lvalue, result>()); resetResult(); const std::list constLvalue = lvalue; - foo(constLvalue); + ASSERT_TRUE(foo.call(constLvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(lvalue, result>()); } diff --git a/Tests/Source/LuaRefTests.cpp b/Tests/Source/LuaRefTests.cpp index 114ab850..7a587590 100644 --- a/Tests/Source/LuaRefTests.cpp +++ b/Tests/Source/LuaRefTests.cpp @@ -183,14 +183,9 @@ TEST_F(LuaRefTests, ValueAccess) " return i " "end"); EXPECT_TRUE(result().isFunction()); - auto fnResult = result()(41); // Replaces result variable - EXPECT_TRUE(fnResult); - EXPECT_TRUE(fnResult.size()); - EXPECT_TRUE(fnResult[0].isNumber()); - ASSERT_EQ(41, fnResult[0].unsafe_cast()); - ASSERT_EQ(41, *fnResult[0].cast()); - ASSERT_EQ(41, luabridge::unsafe_cast(fnResult[0])); - ASSERT_EQ(41, *luabridge::cast(fnResult[0])); + auto fnResult = result().call(41); // Replaces result variable + ASSERT_TRUE(fnResult); + ASSERT_EQ(41, *fnResult); EXPECT_TRUE(result().isNumber()); ASSERT_EQ(42, result()); } @@ -237,11 +232,9 @@ TEST_F(LuaRefTests, DictionaryRead) ASSERT_STREQ("abc", result()[8].unsafe_cast()); EXPECT_TRUE(result()["fn"].isFunction()); - auto fnResult = result()["fn"](41); // Replaces result variable - EXPECT_TRUE(fnResult); - EXPECT_TRUE(fnResult.size()); - EXPECT_TRUE(fnResult[0].isNumber()); - ASSERT_EQ(41, fnResult[0].unsafe_cast()); + auto fnResult = result()["fn"].call(41); // Replaces result variable + ASSERT_TRUE(fnResult); + ASSERT_EQ(41, *fnResult); EXPECT_TRUE(result().isNumber()); ASSERT_EQ(42, result()); } @@ -506,7 +499,7 @@ TEST_F(LuaRefTests, Callable) auto obj = luabridge::getGlobal(L, "obj"); EXPECT_TRUE(obj.isCallable()); EXPECT_EQ(100, obj["i"].unsafe_cast()); - obj(); + EXPECT_TRUE(obj.call()); EXPECT_EQ(200, obj["i"].unsafe_cast()); } @@ -534,7 +527,7 @@ TEST_F(LuaRefTests, CallableWithHandler) return 0; }; - auto result = f.callWithHandler(handler, "badly"); + auto result = f.callWithHandler>(handler, "badly"); EXPECT_FALSE(result); EXPECT_TRUE(calledHandler); EXPECT_TRUE(errorMessage.find("we failed badly") != std::string::npos); @@ -763,7 +756,7 @@ TEST_F(LuaRefTests, HookTesting) )"); for (auto& func : hooklist) { - func.second(0, "x"); + EXPECT_TRUE(func.second.call(0, "x")); } } diff --git a/Tests/Source/MapTests.cpp b/Tests/Source/MapTests.cpp index 30d805ae..30b3da63 100644 --- a/Tests/Source/MapTests.cpp +++ b/Tests/Source/MapTests.cpp @@ -147,14 +147,14 @@ TEST_F(MapTests, PassToFunction) resetResult(); Int2Bool lvalue{{10, false}, {20, true}, {30, true}}; - foo(lvalue); + ASSERT_TRUE(foo.call(lvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(lvalue, result()); resetResult(); const Int2Bool constLvalue = lvalue; - foo(constLvalue); + ASSERT_TRUE(foo.call(constLvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(constLvalue, result()); } diff --git a/Tests/Source/OptionalTests.cpp b/Tests/Source/OptionalTests.cpp index 93e58fb8..3ad73e57 100644 --- a/Tests/Source/OptionalTests.cpp +++ b/Tests/Source/OptionalTests.cpp @@ -255,14 +255,14 @@ TEST_F(OptionalTests, PassToFunction) resetResult(); std::optional lvalue{ 10 }; - foo(lvalue); + ASSERT_TRUE(foo.call(lvalue)); EXPECT_FALSE(result().isNil()); EXPECT_EQ(lvalue, result>()); resetResult(); const std::optional constLvalue = lvalue; - foo(constLvalue); + ASSERT_TRUE(foo.call(constLvalue)); EXPECT_FALSE(result().isNil()); EXPECT_EQ(lvalue, result>()); } @@ -271,14 +271,14 @@ TEST_F(OptionalTests, PassToFunction) resetResult(); std::optional lvalue; - foo(lvalue); + ASSERT_TRUE(foo.call(lvalue)); EXPECT_TRUE(result().isNil()); EXPECT_FALSE(result>()); resetResult(); const std::optional constLvalue = lvalue; - foo(constLvalue); + ASSERT_TRUE(foo.call(constLvalue)); EXPECT_TRUE(result().isNil()); EXPECT_FALSE(result>()); } diff --git a/Tests/Source/PairTests.cpp b/Tests/Source/PairTests.cpp index 67e29562..786fe1a8 100644 --- a/Tests/Source/PairTests.cpp +++ b/Tests/Source/PairTests.cpp @@ -53,7 +53,7 @@ TYPED_TEST_P(PairTest, push) this->runLua("result = nil; function func(data) result = data end"); luabridge::LuaRef func = luabridge::getGlobal(this->L, "func"); - func(data); + ASSERT_TRUE(func.call(data)); Pair const actual = this->result(); ASSERT_EQ(actual, data); diff --git a/Tests/Source/SetTests.cpp b/Tests/Source/SetTests.cpp index fa71d74f..1df87773 100644 --- a/Tests/Source/SetTests.cpp +++ b/Tests/Source/SetTests.cpp @@ -149,14 +149,14 @@ TEST_F(SetTests, PassToFunction) resetResult(); IntSet lvalue{ 10, 20, 30 }; - foo(lvalue); + ASSERT_TRUE(foo.call(lvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(lvalue, result()); resetResult(); const IntSet constLvalue = lvalue; - foo(constLvalue); + ASSERT_TRUE(foo.call(constLvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(constLvalue, result()); } diff --git a/Tests/Source/Tests.cpp b/Tests/Source/Tests.cpp index 205162d0..9003e359 100644 --- a/Tests/Source/Tests.cpp +++ b/Tests/Source/Tests.cpp @@ -397,7 +397,7 @@ TEST_F(LuaBridgeTest, PropertyGetterFailOnUnregisteredClass) #endif } -TEST_F(LuaBridgeTest, CallReturnLuaResult) +TEST_F(LuaBridgeTest, CallReturnTypeResult) { runLua("function f1 (arg0, arg1) end"); runLua("function f2 (arg0, arg1) return arg0; end"); @@ -406,42 +406,32 @@ TEST_F(LuaBridgeTest, CallReturnLuaResult) { auto f1 = luabridge::getGlobal(L, "f1"); - auto result = luabridge::call(f1, 1, 2); - EXPECT_FALSE(result.hasFailed()); - EXPECT_TRUE(result.wasOk()); - EXPECT_EQ(std::error_code(), result.errorCode()); + auto result = f1.call>(1, 2); + EXPECT_TRUE(result); + EXPECT_EQ(std::tuple<>{}, *result); } { auto f2 = luabridge::getGlobal(L, "f2"); - auto result = luabridge::call(f2, 1, 2); - EXPECT_FALSE(result.hasFailed()); - EXPECT_TRUE(result.wasOk()); - EXPECT_EQ(std::error_code(), result.errorCode()); - EXPECT_EQ(1u, result.size()); - EXPECT_EQ(result[0], 1); + auto result = f2.call(1, 2); + ASSERT_TRUE(result); + EXPECT_EQ(1, *result); } { auto f3 = luabridge::getGlobal(L, "f3"); - auto result = luabridge::call(f3, 1, 2); - EXPECT_FALSE(result.hasFailed()); - EXPECT_TRUE(result.wasOk()); - EXPECT_EQ(std::error_code(), result.errorCode()); - EXPECT_EQ(2u, result.size()); - EXPECT_EQ(result[0], 1); - EXPECT_EQ(result[1], 2); + auto result = f3.call>(1, 2); + ASSERT_TRUE(result); + EXPECT_EQ(1, std::get<0>(*result)); + EXPECT_EQ(2, std::get<1>(*result)); } #if ! LUABRIDGE_HAS_EXCEPTIONS { auto f3 = luabridge::getGlobal(L, "f4"); - auto result = luabridge::call(f3); - EXPECT_TRUE(result.hasFailed()); - EXPECT_FALSE(result.wasOk()); - EXPECT_EQ(0u, result.size()); - EXPECT_NE(std::error_code(), result.errorCode()); - EXPECT_NE(std::string::npos, result.errorMessage().find("Something bad happened")); + auto result = f3.call(); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::LuaFunctionCallFailed), result.error()); } #endif } @@ -456,14 +446,13 @@ TEST_F(LuaBridgeTest, InvokePassingUnregisteredClassShouldThrowAndRestoreStack) auto f1 = luabridge::getGlobal(L, "f1"); #if LUABRIDGE_HAS_EXCEPTIONS - EXPECT_THROW(luabridge::call(f1, unregistered), luabridge::LuaException); + EXPECT_THROW(f1.call(unregistered), luabridge::LuaException); #else int stackTop = lua_gettop(L); - auto result = luabridge::call(f1, unregistered); - EXPECT_TRUE(result.hasFailed()); - EXPECT_FALSE(result.wasOk()); - EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::ClassNotRegistered), result.errorCode()); + auto result = f1.call(unregistered); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::ClassNotRegistered), result.error()); EXPECT_EQ(stackTop, lua_gettop(L)); #endif diff --git a/Tests/Source/UnorderedMapTests.cpp b/Tests/Source/UnorderedMapTests.cpp index 72bf98cb..512e91ba 100644 --- a/Tests/Source/UnorderedMapTests.cpp +++ b/Tests/Source/UnorderedMapTests.cpp @@ -121,14 +121,14 @@ TEST_F(UnorderedMapTests, PassToFunction) resetResult(); Int2Bool lvalue{{10, false}, {20, true}, {30, true}}; - foo(lvalue); + ASSERT_TRUE(foo.call(lvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(lvalue, result()); resetResult(); const Int2Bool constLvalue = lvalue; - foo(constLvalue); + ASSERT_TRUE(foo.call(constLvalue)); ASSERT_TRUE(result().isTable()); ASSERT_EQ(constLvalue, result()); } diff --git a/justfile b/justfile index 5aae3bd4..a038c84c 100644 --- a/justfile +++ b/justfile @@ -7,5 +7,10 @@ generate: clean: rm -rf Build +benchmark: + cmake -G Xcode -B Build -DLUABRIDGE_BENCHMARKS=ON . + cmake --build Build --config Release --target LuaBridgeBenchmarks -j8 + ./Build/Benchmarks/Release/LuaBridgeBenchmarks 1000000 + amalgamate: python3 amalgamate.py From 5e20b89de41d3fcebae662bbb500dec658b7182c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 17:48:09 +0200 Subject: [PATCH 03/10] More performance improvements --- Benchmarks/Benchmark.cpp | 33 +- Benchmarks/CMakeLists.txt | 1 + Distribution/LuaBridge/LuaBridge.h | 1001 +++++++++++++++++++++----- Source/LuaBridge/detail/CFunctions.h | 163 ++++- Source/LuaBridge/detail/LuaRef.h | 314 +++++++- Source/LuaBridge/detail/Namespace.h | 28 +- 6 files changed, 1326 insertions(+), 214 deletions(-) diff --git a/Benchmarks/Benchmark.cpp b/Benchmarks/Benchmark.cpp index bfd7cd17..d14bc59a 100644 --- a/Benchmarks/Benchmark.cpp +++ b/Benchmarks/Benchmark.cpp @@ -110,6 +110,8 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) CaseResult freeCall; CaseResult memberCall; CaseResult property; + CaseResult propertySet; + CaseResult propertyGet; CaseResult cppGlobalGet; CaseResult cppGlobalSet; CaseResult cppTableGet; @@ -131,17 +133,21 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; + const std::string propertySetLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i end"; + const std::string propertyGetLoop = "local s = 0; for i = 1, " + std::to_string(iterations) + " do s = s + obj.value end"; empty = runCase("lua_empty_loop", iterations, [&] { doLua(emptyLoop.c_str()); }); freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { doLua(freeCallLoop.c_str()); }); memberCall = runCase("lua_to_cpp_member", iterations, [&] { doLua(memberCallLoop.c_str()); }); property = runCase("lua_to_cpp_property", iterations, [&] { doLua(propertyLoop.c_str()); }); + propertySet = runCase("lua_to_cpp_property_set", iterations, [&] { doLua(propertySetLoop.c_str()); }); + propertyGet = runCase("lua_to_cpp_property_get", iterations, [&] { doLua(propertyGetLoop.c_str()); }); cppGlobalGet = runCase("cpp_table_global_get", iterations, [&] { double x = 0.0; for (std::int64_t i = 0; i < iterations; ++i) - x += static_cast(luabridge::getGlobal(L, "gvalue")); + x += *luabridge::getGlobal(L, "gvalue"); sink = x; }); @@ -161,7 +167,7 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) { double x = 0.0; for (std::int64_t i = 0; i < iterations; ++i) - x += static_cast(tableRef["value"]); + x += tableRef.unsafeRawgetField("value"); sink = x; }); @@ -171,9 +177,9 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) for (std::int64_t i = 0; i < iterations; ++i) { v += 1.0; - tableRef["value"] = v; + tableRef.unsafeRawsetField("value", v); } - sink = static_cast(tableRef["value"]); + sink = tableRef.unsafeRawgetField("value"); }); const auto chainRef = luabridge::getGlobal(L, "chain"); @@ -181,7 +187,7 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) { double x = 0.0; for (std::int64_t i = 0; i < iterations; ++i) - x += static_cast(chainRef["inner"]["value"]); + x += chainRef["inner"].unsafeRawgetField("value"); sink = x; }); @@ -191,9 +197,10 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) for (std::int64_t i = 0; i < iterations; ++i) { v += 1.0; - chainRef["inner"]["value"] = v; + chainRef["inner"].unsafeRawsetField("value", v); } - sink = static_cast(chainRef["inner"]["value"]); + + sink = chainRef["inner"].unsafeRawgetField("value"); }); doLua("function f(a, b) return a + b end"); @@ -219,6 +226,8 @@ void runLuaBridgeBenchmarks(std::int64_t iterations) printResult("LuaBridge", freeCall); printResult("LuaBridge", memberCall); printResult("LuaBridge", property); + printResult("LuaBridge", propertySet); + printResult("LuaBridge", propertyGet); printResult("LuaBridge", cppGlobalGet); printResult("LuaBridge", cppGlobalSet); printResult("LuaBridge", cppTableGet); @@ -259,11 +268,15 @@ void runSol2Benchmarks(std::int64_t iterations) const std::string freeCallLoop = "for i = 1, " + std::to_string(iterations) + " do add(i, i) end"; const std::string memberCallLoop = "for i = 1, " + std::to_string(iterations) + " do obj:inc() end"; const std::string propertyLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i; local x = obj.value end"; + const std::string propertySetLoop = "for i = 1, " + std::to_string(iterations) + " do obj.value = i end"; + const std::string propertyGetLoop = "local s = 0; for i = 1, " + std::to_string(iterations) + " do s = s + obj.value end"; const auto empty = runCase("lua_empty_loop", iterations, [&] { lua.script(emptyLoop); }); const auto freeCall = runCase("lua_to_cpp_free_fn", iterations, [&] { lua.script(freeCallLoop); }); const auto memberCall = runCase("lua_to_cpp_member", iterations, [&] { lua.script(memberCallLoop); }); const auto property = runCase("lua_to_cpp_property", iterations, [&] { lua.script(propertyLoop); }); + const auto propertySet = runCase("lua_to_cpp_property_set", iterations, [&] { lua.script(propertySetLoop); }); + const auto propertyGet = runCase("lua_to_cpp_property_get", iterations, [&] { lua.script(propertyGetLoop); }); const auto cppGlobalGet = runCase("cpp_table_global_get", iterations, [&] { @@ -325,14 +338,14 @@ void runSol2Benchmarks(std::int64_t iterations) }); lua.script("function f(a, b) return a + b end"); - sol::function f = lua["f"]; + sol::protected_function f = lua["f"]; const auto cppToLua = runCase("cpp_to_lua_call", iterations, [&] { for (std::int64_t i = 0; i < iterations; ++i) (void) f(static_cast(i), static_cast(i)); }); - sol::function addFn = lua["add"]; + sol::protected_function addFn = lua["add"]; const auto cppCFunctionThroughLua = runCase("cpp_c_function_through_lua", iterations, [&] { int x = 0; @@ -345,6 +358,8 @@ void runSol2Benchmarks(std::int64_t iterations) printResult("sol2", freeCall); printResult("sol2", memberCall); printResult("sol2", property); + printResult("sol2", propertySet); + printResult("sol2", propertyGet); printResult("sol2", cppGlobalGet); printResult("sol2", cppGlobalSet); printResult("sol2", cppTableGet); diff --git a/Benchmarks/CMakeLists.txt b/Benchmarks/CMakeLists.txt index 5711a117..53246902 100644 --- a/Benchmarks/CMakeLists.txt +++ b/Benchmarks/CMakeLists.txt @@ -32,4 +32,5 @@ target_include_directories(LuaBridgeBenchmarks PRIVATE target_compile_definitions(LuaBridgeBenchmarks PRIVATE LUABRIDGE_BENCHMARK_WITH_SOL2=1 + SOL_ALL_SAFETIES_ON=1 SOL_LUA_VERSION=504) diff --git a/Distribution/LuaBridge/LuaBridge.h b/Distribution/LuaBridge/LuaBridge.h index 8e373dd8..f2e5eafc 100644 --- a/Distribution/LuaBridge/LuaBridge.h +++ b/Distribution/LuaBridge/LuaBridge.h @@ -32,7 +32,6 @@ #include #include #include -#include #include @@ -185,10 +184,7 @@ inline int get_length(lua_State* L, int idx) #else inline int get_length(lua_State* L, int idx) { - lua_len(L, idx); - const int len = static_cast(luaL_checknumber(L, -1)); - lua_pop(L, 1); - return len; + return static_cast(lua_rawlen(L, idx)); } #endif @@ -2606,6 +2602,62 @@ struct TypeResult Expected m_value; }; +template <> +struct TypeResult +{ + TypeResult() noexcept = default; + + TypeResult(std::error_code ec) noexcept + : m_ec(ec) + { + } + + TypeResult(const TypeResult&) noexcept = default; + TypeResult(TypeResult&&) noexcept = default; + TypeResult& operator=(const TypeResult&) noexcept = default; + TypeResult& operator=(TypeResult&&) noexcept = default; + + explicit operator bool() const noexcept + { + return ! m_ec; + } + + void value() const noexcept + { + } + + std::error_code error() const noexcept + { + return m_ec; + } + + const char* error_cstr() const noexcept + { + return detail::ErrorCategory::errorString(m_ec.value()); + } + + operator std::error_code() const noexcept + { + return m_ec; + } + + std::string message() const + { + return m_ec.message(); + } + +#if LUABRIDGE_HAS_EXCEPTIONS + void throw_on_error() const + { + if (m_ec) + throw std::system_error(m_ec); + } +#endif + +private: + std::error_code m_ec; +}; + template inline bool operator==(const TypeResult& lhs, const U& rhs) noexcept { @@ -2706,6 +2758,11 @@ template ().find_first_of('.')> return reinterpret_cast(0xc2b); } +[[nodiscard]] inline const void* getTypeIdentityKey() noexcept +{ + return reinterpret_cast(0xc2c); +} + [[nodiscard]] inline const void* getPropgetKey() noexcept { return reinterpret_cast(0x6e7); @@ -3161,7 +3218,26 @@ class Userdata if (lua_isnil(L, index)) return nullptr; - auto* clazz = getClass(L, index, detail::getConstRegistryKey(), detail::getClassRegistryKey(), canBeConst); + const int absIndex = lua_absindex(L, index); + const auto classId = detail::getClassRegistryKey(); + const auto constId = detail::getConstRegistryKey(); + + if (lua_getmetatable(L, absIndex) && lua_istable(L, -1)) + { + lua_rawgetp_x(L, -1, detail::getTypeIdentityKey()); + const void* identity = lua_touserdata(L, -1); + lua_pop(L, 1); + + if (identity == classId || (canBeConst && identity == constId)) + { + lua_pop(L, 1); + return static_cast(static_cast(lua_touserdata(L, absIndex))->getPointer()); + } + + lua_pop(L, 1); + } + + auto* clazz = getClass(L, absIndex, constId, classId, canBeConst); if (! clazz) return nullptr; @@ -4003,8 +4079,8 @@ struct Stack { if (lua_type(L, index) == LUA_TSTRING) { - std::size_t len; - luaL_checklstring(L, index, &len); + std::size_t len = 0; + lua_tolstring(L, index, &len); return len == 1; } @@ -6375,6 +6451,184 @@ inline int index_metamethod(lua_State* L) } +template +inline int index_metamethod_simple(lua_State* L) +{ +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(L, 3, detail::error_lua_stack_overflow); +#endif + + LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); + + if constexpr (IsObject) + { + if (lua_isuserdata(L, 1)) + { + const char* key = lua_tostring(L, 2); + + const auto rawlookup = [L](int tableIndex) + { + lua_pushvalue(L, 2); + lua_rawget(L, tableIndex); + }; + + rawlookup(lua_upvalueindex(1)); + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); + lua_call(L, 1, 1); + return 1; + } + + lua_pop(L, 1); + + if (key != nullptr && is_metamethod(key)) + { + lua_pushnil(L); + return 1; + } + + rawlookup(lua_upvalueindex(2)); + if (! lua_isnil(L, -1)) + return 1; + + lua_pop(L, 1); + lua_pushnil(L); + return 1; + } + } + else + { + + if (lua_istable(L, 1)) + { + const char* key = lua_tostring(L, 2); + + const auto rawlookup = [L](int tableIndex) + { + lua_pushvalue(L, 2); + lua_rawget(L, tableIndex); + }; + + if (key != nullptr && is_metamethod(key)) + { + lua_pushnil(L); + return 1; + } + + if (lua_istable(L, lua_upvalueindex(2))) + { + rawlookup(lua_upvalueindex(2)); + if (! lua_isnil(L, -1)) + return 1; + + lua_pop(L, 1); + } + + rawlookup(lua_upvalueindex(3)); + if (! lua_isnil(L, -1)) + return 1; + + lua_pop(L, 1); + + rawlookup(lua_upvalueindex(1)); + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); + lua_call(L, 1, 1); + return 1; + } + + lua_pop(L, 1); + lua_pushnil(L); + return 1; + } + } + + lua_getmetatable(L, 1); + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + const char* key = lua_tostring(L, 2); + + const auto rawlookup = [L]() + { + lua_pushvalue(L, 2); + lua_rawget(L, -2); + }; + + if (lua_isuserdata(L, 1)) + { + lua_rawgetp_x(L, -1, getPropgetKey()); + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + rawlookup(); + lua_remove(L, -2); + + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); + lua_pushvalue(L, 1); + lua_call(L, 1, 1); + return 1; + } + + lua_pop(L, 1); + } + if (key != nullptr && is_metamethod(key)) + { + lua_pushnil(L); + return 1; + } + + if (lua_istable(L, 1)) + { + if constexpr (IsObject) + lua_pushvalue(L, 1); + else + push_class_or_const_table(L, -1); + + if (lua_istable(L, -1)) + { + rawlookup(); + lua_remove(L, -2); + if (! lua_isnil(L, -1)) + { + lua_remove(L, -2); + return 1; + } + } + + lua_pop(L, 1); + } + + rawlookup(); + if (! lua_isnil(L, -1)) + { + lua_remove(L, -2); + return 1; + } + + lua_pop(L, 1); + + lua_rawgetp_x(L, -1, getPropgetKey()); + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + rawlookup(); + lua_remove(L, -2); + + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); + lua_pushvalue(L, 1); + lua_call(L, 1, 1); + return 1; + } + + lua_pop(L, 2); + lua_pushnil(L); + return 1; +} + inline std::optional try_call_newindex_fallback(lua_State* L) { LUABRIDGE_ASSERT(lua_istable(L, -1)); @@ -6605,6 +6859,85 @@ inline int newindex_metamethod(lua_State* L) return 0; } +template +inline int newindex_metamethod_simple(lua_State* L) +{ +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(L, 3, detail::error_lua_stack_overflow); +#endif + + LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); + + if constexpr (IsObject) + { + if (lua_isuserdata(L, 1)) + { + const char* key = lua_tostring(L, 2); + + lua_pushvalue(L, 2); + lua_rawget(L, lua_upvalueindex(1)); + + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); + lua_pushvalue(L, 3); + lua_call(L, 2, 0); + return 0; + } + + luaL_error(L, "no writable member '%s'", key); + return 0; + } + } + else + { + + if (lua_istable(L, 1)) + { + const char* key = lua_tostring(L, 2); + + lua_pushvalue(L, 2); + lua_rawget(L, lua_upvalueindex(1)); + + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 3); + lua_call(L, 1, 0); + return 0; + } + + luaL_error(L, "no writable member '%s'", key); + return 0; + } + } + + lua_getmetatable(L, 1); + LUABRIDGE_ASSERT(lua_istable(L, -1)); + + const char* key = lua_tostring(L, 2); + + lua_rawgetp_x(L, -1, getPropsetKey()); + if (! lua_istable(L, -1)) + luaL_error(L, "no member named '%s'", key); + + lua_pushvalue(L, 2); + lua_rawget(L, -2); + lua_remove(L, -2); + + if (lua_iscfunction(L, -1)) + { + lua_remove(L, -2); + if constexpr (IsObject) + lua_pushvalue(L, 1); + lua_pushvalue(L, 3); + lua_call(L, IsObject ? 2 : 1, 0); + return 0; + } + + luaL_error(L, "no writable member '%s'", key); + return 0; +} + inline int read_only_error(lua_State* L) { raise_lua_error(L, "'%s' is read-only", lua_tostring(L, lua_upvalueindex(1))); @@ -6808,19 +7141,55 @@ inline void add_property_setter(lua_State* L, const char* name, int tableIndex) lua_pop(L, 2); } -template -struct function +template +decltype(auto) invoke_callable_from_stack_impl(lua_State* L, F&& func, std::index_sequence) { - template - static int call(lua_State* L, F&& func) - { - Result result; + return std::invoke( + std::forward(func), + unwrap_argument_or_error>(L, Indices, Start)...); +} -#if LUABRIDGE_HAS_EXCEPTIONS - try - { -#endif - result = Stack::push(L, std::apply(std::forward(func), make_arguments_list(L))); +template +decltype(auto) invoke_callable_from_stack(lua_State* L, F&& func) +{ + return invoke_callable_from_stack_impl( + L, + std::forward(func), + std::make_index_sequence>()); +} + +template +decltype(auto) invoke_member_callable_from_stack_impl(lua_State* L, T* ptr, F&& func, std::index_sequence) +{ + return std::invoke( + std::forward(func), + ptr, + unwrap_argument_or_error>(L, Indices, Start)...); +} + +template +decltype(auto) invoke_member_callable_from_stack(lua_State* L, T* ptr, F&& func) +{ + return invoke_member_callable_from_stack_impl( + L, + ptr, + std::forward(func), + std::make_index_sequence>()); +} + +template +struct function +{ + template + static int call(lua_State* L, F&& func) + { + Result result; + +#if LUABRIDGE_HAS_EXCEPTIONS + try + { +#endif + result = Stack::push(L, invoke_callable_from_stack(L, std::forward(func))); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -6845,9 +7214,7 @@ struct function try { #endif - auto f = [ptr, func = std::forward(func)](auto&&... args) -> ReturnType { return (ptr->*func)(std::forward(args)...); }; - - result = Stack::push(L, std::apply(f, make_arguments_list(L))); + result = Stack::push(L, invoke_member_callable_from_stack(L, ptr, std::forward(func))); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -6874,7 +7241,7 @@ struct function try { #endif - std::apply(std::forward(func), make_arguments_list(L)); + invoke_callable_from_stack(L, std::forward(func)); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -6894,9 +7261,7 @@ struct function try { #endif - auto f = [ptr, func = std::forward(func)](auto&&... args) { (ptr->*func)(std::forward(args)...); }; - - std::apply(f, make_arguments_list(L)); + invoke_member_callable_from_stack(L, ptr, std::forward(func)); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -8014,7 +8379,8 @@ bool setGlobal(lua_State* L, T&& t, const char* name) namespace luabridge { -class LuaResult; +template +class LuaFunction; struct LuaNil { @@ -8371,14 +8737,20 @@ class LuaRefBase return (appendOne(vs) && ...); } - template - LuaResult operator()(Args&&... args) const; + template + TypeResult call(Args&&... args) const; template - LuaResult call(Args&&... args) const; + TypeResult operator()(Args&&... args) const; + + template + TypeResult callWithHandler(F&& errorHandler, Args&&... args) const; template - LuaResult callWithHandler(F&& errorHandler, Args&&... args) const; + TypeResult callWithHandler(F&& errorHandler, Args&&... args) const; + + template + LuaFunction callable() const; protected: lua_State* m_L = nullptr; @@ -8396,6 +8768,10 @@ class LuaRef : public LuaRefBase { friend class LuaRef; + struct AdoptTableRef + { + }; + public: TableItem(lua_State* L, int tableRef) @@ -8410,6 +8786,34 @@ class LuaRef : public LuaRefBase m_tableRef = luaL_ref(L, LUA_REGISTRYINDEX); } + template + TableItem(lua_State* L, int tableRef, const char (&key)[N]) + : LuaRefBase(L) + , m_keyLiteral(key) + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 1, detail::error_lua_stack_overflow); +#endif + + lua_rawgeti(m_L, LUA_REGISTRYINDEX, tableRef); + m_tableRef = luaL_ref(L, LUA_REGISTRYINDEX); + } + + TableItem(lua_State* L, int tableRef, AdoptTableRef) + : LuaRefBase(L) + , m_tableRef(tableRef) + , m_keyRef(luaL_ref(L, LUA_REGISTRYINDEX)) + { + } + + template + TableItem(lua_State* L, int tableRef, AdoptTableRef, const char (&key)[N]) + : LuaRefBase(L) + , m_tableRef(tableRef) + , m_keyLiteral(key) + { + } + TableItem(const TableItem& other) : LuaRefBase(other.m_L) { @@ -8421,8 +8825,12 @@ class LuaRef : public LuaRefBase lua_rawgeti(m_L, LUA_REGISTRYINDEX, other.m_tableRef); m_tableRef = luaL_ref(m_L, LUA_REGISTRYINDEX); - lua_rawgeti(m_L, LUA_REGISTRYINDEX, other.m_keyRef); - m_keyRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + m_keyLiteral = other.m_keyLiteral; + if (other.m_keyRef != LUA_NOREF) + { + lua_rawgeti(m_L, LUA_REGISTRYINDEX, other.m_keyRef); + m_keyRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + } } ~TableItem() @@ -8445,12 +8853,23 @@ class LuaRef : public LuaRefBase const StackRestore stackRestore(m_L); lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_tableRef); - lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + if (m_keyLiteral != nullptr) + { + if (! Stack::push(m_L, v)) + return *this; - if (! Stack::push(m_L, v)) - return *this; + lua_setfield(m_L, -2, m_keyLiteral); + } + else + { + lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + + if (! Stack::push(m_L, v)) + return *this; + + lua_settable(m_L, -3); + } - lua_settable(m_L, -3); return *this; } @@ -8465,12 +8884,25 @@ class LuaRef : public LuaRefBase const StackRestore stackRestore(m_L); lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_tableRef); - lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + if (m_keyLiteral != nullptr) + { + lua_pushstring(m_L, m_keyLiteral); - if (! Stack::push(m_L, v)) - return *this; + if (! Stack::push(m_L, v)) + return *this; + + lua_rawset(m_L, -3); + } + else + { + lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + + if (! Stack::push(m_L, v)) + return *this; + + lua_rawset(m_L, -3); + } - lua_rawset(m_L, -3); return *this; } @@ -8489,15 +8921,47 @@ class LuaRef : public LuaRefBase #endif lua_rawgeti(L, LUA_REGISTRYINDEX, m_tableRef); - lua_rawgeti(L, LUA_REGISTRYINDEX, m_keyRef); - lua_gettable(L, -2); + + if (m_keyLiteral != nullptr) + { + lua_getfield(L, -1, m_keyLiteral); + } + else + { + lua_rawgeti(L, LUA_REGISTRYINDEX, m_keyRef); + lua_gettable(L, -2); + } + lua_remove(L, -2); } template TableItem operator[](const T& key) const { - return LuaRef(*this)[key]; + const StackRestore stackRestore(m_L); + + push(m_L); + + if (! Stack::push(m_L, key)) + lua_pushnil(m_L); + + lua_pushvalue(m_L, -2); + const auto tableRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + lua_remove(m_L, -2); + + return TableItem(m_L, tableRef, AdoptTableRef{}); + } + + template + TableItem operator[](const char (&key)[N]) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + + const auto tableRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + + return TableItem(m_L, tableRef, AdoptTableRef{}, key); } template @@ -8509,6 +8973,7 @@ class LuaRef : public LuaRefBase private: int m_tableRef = LUA_NOREF; int m_keyRef = LUA_NOREF; + const char* m_keyLiteral = nullptr; }; friend struct Stack; @@ -8714,6 +9179,12 @@ class LuaRef : public LuaRefBase return TableItem(m_L, m_ref); } + template + TableItem operator[](const char (&key)[N]) const + { + return TableItem(m_L, m_ref, key); + } + template LuaRef rawget(const T& key) const { @@ -8728,6 +9199,91 @@ class LuaRef : public LuaRefBase return LuaRef(m_L, FromStack()); } + template + TypeResult getField(const char* key) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + lua_getfield(m_L, -1, key); + + return Stack::get(m_L, -1); + } + + template + bool setField(const char* key, T&& value) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + + if (! Stack>::push(m_L, std::forward(value))) + return false; + + lua_setfield(m_L, -2, key); + return true; + } + + template + TypeResult rawgetField(const char* key) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + lua_pushstring(m_L, key); + lua_rawget(m_L, -2); + + return Stack::get(m_L, -1); + } + + template + bool rawsetField(const char* key, T&& value) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + lua_pushstring(m_L, key); + + if (! Stack>::push(m_L, std::forward(value))) + return false; + + lua_rawset(m_L, -3); + return true; + } + + template + T unsafeRawgetField(const char* key) const + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 3, detail::error_lua_stack_overflow); +#endif + + push(m_L); + lua_pushstring(m_L, key); + lua_rawget(m_L, -2); + + auto result = Stack::get(m_L, -1); + lua_pop(m_L, 2); + + return result.value(); + } + + template + void unsafeRawsetField(const char* key, T&& value) const + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 3, detail::error_lua_stack_overflow); +#endif + + push(m_L); + lua_pushstring(m_L, key); + [[maybe_unused]] const auto pushed = Stack>::push(m_L, std::forward(value)); + LUABRIDGE_ASSERT(static_cast(pushed)); + + lua_rawset(m_L, -3); + lua_pop(m_L, 1); + } + std::size_t hash() const { std::size_t value; @@ -8892,138 +9448,99 @@ struct hash namespace luabridge { -class LuaResult -{ -public: - - explicit operator bool() const noexcept - { - return !m_ec; - } - - bool wasOk() const noexcept - { - return !m_ec; - } - - bool hasFailed() const noexcept - { - return !!m_ec; - } - - std::error_code errorCode() const noexcept - { - return m_ec; - } - - std::string errorMessage() const noexcept - { - if (std::holds_alternative(m_data)) - { - const auto& message = std::get(m_data); - return message.empty() ? m_ec.message() : message; - } - - return {}; - } +namespace detail { - std::size_t size() const noexcept - { - if (std::holds_alternative>(m_data)) - return std::get>(m_data).size(); +template +struct IsTuple : std::false_type +{ +}; - return 0; - } +template +struct IsTuple> : std::true_type +{ +}; - LuaRef operator[](std::size_t index) const - { - LUABRIDGE_ASSERT(m_ec == std::error_code()); +template +TypeResult decodeTupleResult(lua_State* L, int firstResultIndex, std::index_sequence) +{ + Tuple value; + std::error_code ec; - if (std::holds_alternative>(m_data)) + const bool ok = + (([&]() { - const auto& values = std::get>(m_data); + using ElementType = std::tuple_element_t; - LUABRIDGE_ASSERT(index < values.size()); - return values[index]; - } + auto element = Stack::get(L, firstResultIndex + static_cast(Indices)); + if (! element) + { + ec = element.error(); + return false; + } - return LuaRef(m_L); - } + std::get(value) = std::move(*element); + return true; + }()) + && ...); -private: - template - friend LuaResult call(const LuaRef&, Args&&...); + if (! ok) + return ec; - template - friend LuaResult callWithHandler(const LuaRef&, F&&, Args&&...); + return value; +} - static LuaResult errorFromStack(lua_State* L, std::error_code ec) +template +TypeResult decodeCallResult(lua_State* L, int firstResultIndex, int numReturnedValues) +{ + if constexpr (std::is_same_v || std::is_same_v>) { - auto errorString = lua_tostring(L, -1); - lua_pop(L, 1); + if (numReturnedValues != 0) + return makeErrorCode(ErrorCode::InvalidTableSizeInCast); - return LuaResult(L, ec, errorString ? errorString : ec.message()); + return {}; } - - static LuaResult valuesFromStack(lua_State* L, int stackTop) + else + if constexpr (IsTuple::value) { - std::vector values; - - const int numReturnedValues = lua_gettop(L) - stackTop; - if (numReturnedValues > 0) - { - values.reserve(numReturnedValues); - - for (int index = numReturnedValues; index > 0; --index) - values.emplace_back(LuaRef::fromStack(L, -index)); - - lua_pop(L, numReturnedValues); - } + constexpr auto expectedSize = static_cast(std::tuple_size_v); + if (numReturnedValues != expectedSize) + return makeErrorCode(ErrorCode::InvalidTableSizeInCast); - return LuaResult(L, std::move(values)); + return decodeTupleResult(L, firstResultIndex, std::make_index_sequence>{}); } - - LuaResult(lua_State* L, std::error_code ec, std::string_view errorString) - : m_L(L) - , m_ec(ec) - , m_data(std::string(errorString)) + else { - } + if (numReturnedValues < 1) + return makeErrorCode(ErrorCode::InvalidTypeCast); - explicit LuaResult(lua_State* L, std::vector values) noexcept - : m_L(L) - , m_data(std::move(values)) - { + return Stack::get(L, firstResultIndex); } +} - lua_State* m_L = nullptr; - std::error_code m_ec; - std::variant, std::string> m_data; -}; +} -template -LuaResult callWithHandler(const LuaRef& object, F&& errorHandler, Args&&... args) +template +TypeResult callWithHandler(const Ref& object, F&& errorHandler, Args&&... args) { static constexpr bool isValidHandler = !std::is_same_v, detail::remove_cvref_t>; lua_State* L = object.state(); - const int stackTop = lua_gettop(L); + const StackRestore stackRestore(L); + const int initialTop = lua_gettop(L); if constexpr (isValidHandler) - detail::push_function(L, std::forward(errorHandler), ""); + detail::push_function(L, std::forward(errorHandler), ""); - object.push(); + object.push(); { - const auto [result, index] = detail::push_arguments(L, std::forward_as_tuple(args...)); + const auto [result, index] = detail::push_arguments(L, std::forward_as_tuple(args...)); if (! result) - { - lua_pop(L, static_cast(index) + 1); - return LuaResult(L, result, result.message()); - } + return result.error(); } - const int code = lua_pcall(L, sizeof...(Args), LUA_MULTRET, isValidHandler ? (-static_cast(sizeof...(Args)) - 2) : 0); + const int messageHandlerIndex = isValidHandler ? (initialTop + 1) : 0; + const int code = lua_pcall(L, sizeof...(Args), LUA_MULTRET, messageHandlerIndex); if (code != LUABRIDGE_LUA_OK) { auto ec = makeErrorCode(ErrorCode::LuaFunctionCallFailed); @@ -9036,18 +9553,79 @@ LuaResult callWithHandler(const LuaRef& object, F&& errorHandler, Args&&... args } #endif - return LuaResult::errorFromStack(L, ec); + lua_pop(L, 1); + return ec; } - return LuaResult::valuesFromStack(L, stackTop); + if constexpr (isValidHandler) + lua_remove(L, initialTop + 1); + + const int firstResultIndex = initialTop + 1; + const int numReturnedValues = lua_gettop(L) - initialTop; + return detail::decodeCallResult(L, firstResultIndex, numReturnedValues); } -template -LuaResult call(const LuaRef& object, Args&&... args) +template +TypeResult callWithHandler(const Ref& object, F&& errorHandler, Args&&... args) +{ + return callWithHandler(object, std::forward(errorHandler), std::forward(args)...); +} + +template +TypeResult call(const Ref& object, Args&&... args) { - return callWithHandler(object, std::ignore, std::forward(args)...); + return callWithHandler(object, std::ignore, std::forward(args)...); } +template +class LuaFunction; + +template +class LuaFunction +{ +public: + LuaFunction() = default; + + explicit LuaFunction(const LuaRef& function) + : m_function(function) + { + } + + explicit LuaFunction(LuaRef&& function) + : m_function(std::move(function)) + { + } + + [[nodiscard]] TypeResult operator()(Args... args) const + { + return call(std::forward(args)...); + } + + [[nodiscard]] TypeResult call(Args... args) const + { + return luabridge::call(m_function, std::forward(args)...); + } + + template + [[nodiscard]] TypeResult callWithHandler(F&& errorHandler, Args... args) const + { + return luabridge::callWithHandler(m_function, std::forward(errorHandler), std::forward(args)...); + } + + [[nodiscard]] bool isValid() const + { + return m_function.isCallable(); + } + + [[nodiscard]] const LuaRef& ref() const + { + return m_function; + } + +private: + LuaRef m_function; +}; + inline int pcall(lua_State* L, int nargs = 0, int nresults = 0, int msgh = 0) { const int code = lua_pcall(L, nargs, nresults, msgh); @@ -9061,24 +9639,40 @@ inline int pcall(lua_State* L, int nargs = 0, int nresults = 0, int msgh = 0) } template -template -LuaResult LuaRefBase::operator()(Args&&... args) const +template +TypeResult LuaRefBase::call(Args&&... args) const { - return luabridge::call(*this, std::forward(args)...); + return luabridge::call(impl(), std::forward(args)...); } template template -LuaResult LuaRefBase::call(Args&&... args) const +TypeResult LuaRefBase::operator()(Args&&... args) const { - return luabridge::call(*this, std::forward(args)...); + return call(std::forward(args)...); +} + +template +template +TypeResult LuaRefBase::callWithHandler(F&& errorHandler, Args&&... args) const +{ + return luabridge::callWithHandler(impl(), std::forward(errorHandler), std::forward(args)...); } template template -LuaResult LuaRefBase::callWithHandler(F&& errorHandler, Args&&... args) const +TypeResult LuaRefBase::callWithHandler(F&& errorHandler, Args&&... args) const +{ + return callWithHandler(std::forward(errorHandler), std::forward(args)...); +} + +template +template +LuaFunction LuaRefBase::callable() const { - return luabridge::callWithHandler(*this, std::forward(errorHandler), std::forward(args)...); + const StackRestore stackRestore(m_L); + impl().push(m_L); + return LuaFunction(LuaRef::fromStack(m_L)); } } @@ -9341,7 +9935,57 @@ class Namespace : public detail::Registrar using Registrar::operator=; protected: - + void setObjectMetaMethods(int tableIndex, bool simple) + { + tableIndex = lua_absindex(L, tableIndex); + + if (simple) + { + lua_rawgetp_x(L, tableIndex, detail::getPropgetKey()); + lua_pushvalue(L, tableIndex); + lua_pushcclosure_x(L, &detail::index_metamethod_simple, "__index", 2); + rawsetfield(L, tableIndex, "__index"); + + lua_rawgetp_x(L, tableIndex, detail::getPropsetKey()); + lua_pushcclosure_x(L, &detail::newindex_metamethod_simple, "__newindex", 1); + rawsetfield(L, tableIndex, "__newindex"); + } + else + { + lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); + rawsetfield(L, tableIndex, "__index"); + + lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); + rawsetfield(L, tableIndex, "__newindex"); + } + } + + void setStaticMetaMethods(int tableIndex, bool simple) + { + tableIndex = lua_absindex(L, tableIndex); + + if (simple) + { + lua_rawgetp_x(L, tableIndex, detail::getPropgetKey()); + lua_rawgetp_x(L, tableIndex, detail::getClassKey()); + lua_pushvalue(L, tableIndex); + lua_pushcclosure_x(L, &detail::index_metamethod_simple, "__index", 3); + rawsetfield(L, tableIndex, "__index"); + + lua_rawgetp_x(L, tableIndex, detail::getPropsetKey()); + lua_pushcclosure_x(L, &detail::newindex_metamethod_simple, "__newindex", 1); + rawsetfield(L, tableIndex, "__newindex"); + } + else + { + lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); + rawsetfield(L, tableIndex, "__index"); + + lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); + rawsetfield(L, tableIndex, "__newindex"); + } + } + void createConstTable(const char* name, bool trueConst, Options options) { LUABRIDGE_ASSERT(name != nullptr); @@ -9358,15 +10002,11 @@ class Namespace : public detail::Registrar lua_pushstring(L, type_name.c_str()); lua_rawsetp_x(L, -2, detail::getTypeKey()); - lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); - rawsetfield(L, -2, "__index"); - - lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); - rawsetfield(L, -2, "__newindex"); - lua_newtable(L); lua_rawsetp_x(L, -2, detail::getPropgetKey()); + setObjectMetaMethods(-1, ! options.test(extensibleClass)); + if (! options.test(visibleMetatables)) { lua_pushboolean(L, 0); @@ -9388,6 +10028,9 @@ class Namespace : public detail::Registrar lua_pushvalue(L, -1); lua_rawsetp_x(L, -3, detail::getClassKey()); + + if (! options.test(extensibleClass)) + setObjectMetaMethods(-1, true); } void createStaticTable(const char* name, Options options) @@ -9404,12 +10047,6 @@ class Namespace : public detail::Registrar pushunsigned(L, options.toUnderlying()); lua_rawsetp_x(L, -2, detail::getClassOptionsKey()); - lua_pushcfunction_x(L, &detail::index_metamethod, "__index"); - rawsetfield(L, -2, "__index"); - - lua_pushcfunction_x(L, &detail::newindex_metamethod, "__newindex"); - rawsetfield(L, -2, "__newindex"); - lua_newtable(L); lua_rawsetp_x(L, -2, detail::getPropgetKey()); @@ -9419,6 +10056,8 @@ class Namespace : public detail::Registrar lua_pushvalue(L, -2); lua_rawsetp_x(L, -2, detail::getClassKey()); + setStaticMetaMethods(-1, ! options.test(extensibleClass)); + if (! options.test(visibleMetatables)) { lua_pushboolean(L, 0); @@ -9465,6 +10104,12 @@ class Namespace : public detail::Registrar createClassTable(name, options); ++m_stackSize; + + lua_pushlightuserdata(L, const_cast(detail::getConstRegistryKey())); + lua_rawsetp_x(L, -3, detail::getTypeIdentityKey()); + lua_pushlightuserdata(L, const_cast(detail::getClassRegistryKey())); + lua_rawsetp_x(L, -2, detail::getTypeIdentityKey()); + #if !defined(LUABRIDGE_ON_LUAU) lua_pushcfunction_x(L, &detail::gc_metamethod, "__gc"); rawsetfield(L, -2, "__gc"); @@ -9526,6 +10171,12 @@ class Namespace : public detail::Registrar createClassTable(name, options); ++m_stackSize; + + lua_pushlightuserdata(L, const_cast(detail::getConstRegistryKey())); + lua_rawsetp_x(L, -3, detail::getTypeIdentityKey()); + lua_pushlightuserdata(L, const_cast(detail::getClassRegistryKey())); + lua_rawsetp_x(L, -2, detail::getTypeIdentityKey()); + #if !defined(LUABRIDGE_ON_LUAU) lua_pushcfunction_x(L, &detail::gc_metamethod, "__gc"); rawsetfield(L, -2, "__gc"); @@ -9562,6 +10213,10 @@ class Namespace : public detail::Registrar lua_rawsetp_x(L, -4, detail::getParentKey()); lua_rawsetp_x(L, -2, detail::getParentKey()); + setObjectMetaMethods(-3, false); + setObjectMetaMethods(-2, false); + setStaticMetaMethods(-1, false); + lua_pushvalue(L, -1); lua_rawsetp_x(L, LUA_REGISTRYINDEX, detail::getStaticRegistryKey()); lua_pushvalue(L, -2); @@ -9673,6 +10328,7 @@ class Namespace : public detail::Registrar lua_newuserdata_aligned(L, std::move(function)); lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__index", 1); lua_rawsetp_x(L, -2, detail::getStaticIndexFallbackKey()); + setStaticMetaMethods(-1, false); return *this; } @@ -9686,6 +10342,7 @@ class Namespace : public detail::Registrar lua_pushlightuserdata(L, reinterpret_cast(idxf)); lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__index", 1); lua_rawsetp_x(L, -2, detail::getStaticIndexFallbackKey()); + setStaticMetaMethods(-1, false); return *this; } @@ -9702,6 +10359,7 @@ class Namespace : public detail::Registrar lua_newuserdata_aligned(L, std::move(function)); lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__newindex", 1); lua_rawsetp_x(L, -2, detail::getStaticNewIndexFallbackKey()); + setStaticMetaMethods(-1, false); return *this; } @@ -9715,6 +10373,7 @@ class Namespace : public detail::Registrar lua_pushlightuserdata(L, reinterpret_cast(idxf)); lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__newindex", 1); lua_rawsetp_x(L, -2, detail::getStaticNewIndexFallbackKey()); + setStaticMetaMethods(-1, false); return *this; } @@ -10103,6 +10762,7 @@ class Namespace : public detail::Registrar lua_newuserdata_aligned(L, std::move(function)); lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__index", 1); lua_rawsetp_x(L, -3, detail::getIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -10116,6 +10776,7 @@ class Namespace : public detail::Registrar lua_pushlightuserdata(L, reinterpret_cast(idxf)); lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__index", 1); lua_rawsetp_x(L, -3, detail::getIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -10129,6 +10790,7 @@ class Namespace : public detail::Registrar new (lua_newuserdata_x(L, sizeof(MemFnPtr))) MemFnPtr(idxf); lua_pushcclosure_x(L, &detail::invoke_member_function, "__index", 1); lua_rawsetp_x(L, -3, detail::getIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -10145,6 +10807,7 @@ class Namespace : public detail::Registrar lua_newuserdata_aligned(L, std::move(function)); lua_pushcclosure_x(L, &detail::invoke_proxy_functor, "__newindex", 1); lua_rawsetp_x(L, -3, detail::getNewIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -10158,6 +10821,7 @@ class Namespace : public detail::Registrar lua_pushlightuserdata(L, reinterpret_cast(idxf)); lua_pushcclosure_x(L, &detail::invoke_proxy_function, "__newindex", 1); lua_rawsetp_x(L, -3, detail::getNewIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } @@ -10171,6 +10835,7 @@ class Namespace : public detail::Registrar new (lua_newuserdata_x(L, sizeof(MemFnPtr))) MemFnPtr(idxf); lua_pushcclosure_x(L, &detail::invoke_member_function, "__newindex", 1); lua_rawsetp_x(L, -3, detail::getNewIndexFallbackKey()); + setObjectMetaMethods(-2, false); return *this; } diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index 3ba3f295..5904db74 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -437,20 +437,102 @@ inline int index_metamethod_simple(lua_State* L) LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); + // Fast path for simple userdata objects: when registration provides propget and + // method tables as closure upvalues, avoid metatable/pointer-key lookups. + if constexpr (IsObject) + { + if (lua_isuserdata(L, 1)) + { + const char* key = lua_tostring(L, 2); + + const auto rawlookup = [L](int tableIndex) + { + lua_pushvalue(L, 2); + lua_rawget(L, tableIndex); + }; + + rawlookup(lua_upvalueindex(1)); // Stack: getter | nil + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); // Stack: getter, self + lua_call(L, 1, 1); // Stack: value + return 1; + } + + lua_pop(L, 1); + + if (key != nullptr && is_metamethod(key)) + { + lua_pushnil(L); + return 1; + } + + rawlookup(lua_upvalueindex(2)); // Stack: value | nil + if (! lua_isnil(L, -1)) + return 1; + + lua_pop(L, 1); + lua_pushnil(L); + return 1; + } + } + else + { + // Fast path for simple static tables: capture propget/class/metatable as upvalues. + if (lua_istable(L, 1)) + { + const char* key = lua_tostring(L, 2); + + const auto rawlookup = [L](int tableIndex) + { + lua_pushvalue(L, 2); + lua_rawget(L, tableIndex); + }; + + if (key != nullptr && is_metamethod(key)) + { + lua_pushnil(L); + return 1; + } + + if (lua_istable(L, lua_upvalueindex(2))) + { + rawlookup(lua_upvalueindex(2)); // Stack: value | nil + if (! lua_isnil(L, -1)) + return 1; + + lua_pop(L, 1); + } + + rawlookup(lua_upvalueindex(3)); // Stack: value | nil + if (! lua_isnil(L, -1)) + return 1; + + lua_pop(L, 1); + + rawlookup(lua_upvalueindex(1)); // Stack: getter | nil + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); // Stack: getter, self + lua_call(L, 1, 1); // Stack: value + return 1; + } + + lua_pop(L, 1); + lua_pushnil(L); + return 1; + } + } + lua_getmetatable(L, 1); // Stack: mt LUABRIDGE_ASSERT(lua_istable(L, -1)); const char* key = lua_tostring(L, 2); - const auto rawlookup = [L, key]() + const auto rawlookup = [L]() { - if (key != nullptr) - rawgetfield(L, -1, key); - else - { - lua_pushvalue(L, 2); - lua_rawget(L, -2); - } + lua_pushvalue(L, 2); + lua_rawget(L, -2); }; // For userdata instance property access, checking propget first avoids an always-miss lookup @@ -793,6 +875,62 @@ inline int newindex_metamethod_simple(lua_State* L) LUABRIDGE_ASSERT(lua_istable(L, 1) || lua_isuserdata(L, 1)); + // Fast path for simple userdata objects: propset table is captured as upvalue. + if constexpr (IsObject) + { + if (lua_isuserdata(L, 1)) + { + const char* key = lua_tostring(L, 2); + + if (! lua_istable(L, lua_upvalueindex(1))) + { + luaL_error(L, "no writable member '%s'", key); + return 0; + } + + lua_pushvalue(L, 2); // Stack: key + lua_rawget(L, lua_upvalueindex(1)); // Stack: setter | nil + + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 1); // Stack: setter, self + lua_pushvalue(L, 3); // Stack: setter, self, value + lua_call(L, 2, 0); + return 0; + } + + luaL_error(L, "no writable member '%s'", key); + return 0; + } + } + else + { + // Fast path for simple static tables: propset table is captured as upvalue. + if (lua_istable(L, 1)) + { + const char* key = lua_tostring(L, 2); + + if (! lua_istable(L, lua_upvalueindex(1))) + { + luaL_error(L, "no writable member '%s'", key); + return 0; + } + + lua_pushvalue(L, 2); // Stack: key + lua_rawget(L, lua_upvalueindex(1)); // Stack: setter | nil + + if (lua_iscfunction(L, -1)) + { + lua_pushvalue(L, 3); // Stack: setter, value + lua_call(L, 1, 0); + return 0; + } + + luaL_error(L, "no writable member '%s'", key); + return 0; + } + } + lua_getmetatable(L, 1); // Stack: mt LUABRIDGE_ASSERT(lua_istable(L, -1)); @@ -802,13 +940,8 @@ inline int newindex_metamethod_simple(lua_State* L) if (! lua_istable(L, -1)) luaL_error(L, "no member named '%s'", key); - if (key != nullptr) - rawgetfield(L, -1, key); // Stack: mt, ps, setter | nil - else - { - lua_pushvalue(L, 2); // Stack: mt, ps, key - lua_rawget(L, -2); // Stack: mt, ps, setter | nil - } + lua_pushvalue(L, 2); // Stack: mt, ps, key + lua_rawget(L, -2); // Stack: mt, ps, setter | nil lua_remove(L, -2); // Stack: mt, setter | nil if (lua_iscfunction(L, -1)) diff --git a/Source/LuaBridge/detail/LuaRef.h b/Source/LuaBridge/detail/LuaRef.h index 1edc860a..7f56495f 100644 --- a/Source/LuaBridge/detail/LuaRef.h +++ b/Source/LuaBridge/detail/LuaRef.h @@ -676,6 +676,10 @@ class LuaRef : public LuaRefBase { friend class LuaRef; + struct AdoptTableRef + { + }; + public: //========================================================================================= /** @@ -699,6 +703,34 @@ class LuaRef : public LuaRefBase m_tableRef = luaL_ref(L, LUA_REGISTRYINDEX); } + template + TableItem(lua_State* L, int tableRef, const char (&key)[N]) + : LuaRefBase(L) + , m_keyLiteral(key) + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 1, detail::error_lua_stack_overflow); +#endif + + lua_rawgeti(m_L, LUA_REGISTRYINDEX, tableRef); + m_tableRef = luaL_ref(L, LUA_REGISTRYINDEX); + } + + TableItem(lua_State* L, int tableRef, AdoptTableRef) + : LuaRefBase(L) + , m_tableRef(tableRef) + , m_keyRef(luaL_ref(L, LUA_REGISTRYINDEX)) + { + } + + template + TableItem(lua_State* L, int tableRef, AdoptTableRef, const char (&key)[N]) + : LuaRefBase(L) + , m_tableRef(tableRef) + , m_keyLiteral(key) + { + } + //========================================================================================= /** * @brief Create a TableItem via copy constructor. @@ -719,8 +751,12 @@ class LuaRef : public LuaRefBase lua_rawgeti(m_L, LUA_REGISTRYINDEX, other.m_tableRef); m_tableRef = luaL_ref(m_L, LUA_REGISTRYINDEX); - lua_rawgeti(m_L, LUA_REGISTRYINDEX, other.m_keyRef); - m_keyRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + m_keyLiteral = other.m_keyLiteral; + if (other.m_keyRef != LUA_NOREF) + { + lua_rawgeti(m_L, LUA_REGISTRYINDEX, other.m_keyRef); + m_keyRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + } } //========================================================================================= @@ -761,12 +797,23 @@ class LuaRef : public LuaRefBase const StackRestore stackRestore(m_L); lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_tableRef); - lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + if (m_keyLiteral != nullptr) + { + if (! Stack::push(m_L, v)) + return *this; - if (! Stack::push(m_L, v)) - return *this; + lua_setfield(m_L, -2, m_keyLiteral); + } + else + { + lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + + if (! Stack::push(m_L, v)) + return *this; + + lua_settable(m_L, -3); + } - lua_settable(m_L, -3); return *this; } @@ -793,12 +840,25 @@ class LuaRef : public LuaRefBase const StackRestore stackRestore(m_L); lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_tableRef); - lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + if (m_keyLiteral != nullptr) + { + lua_pushstring(m_L, m_keyLiteral); - if (! Stack::push(m_L, v)) - return *this; + if (! Stack::push(m_L, v)) + return *this; + + lua_rawset(m_L, -3); + } + else + { + lua_rawgeti(m_L, LUA_REGISTRYINDEX, m_keyRef); + + if (! Stack::push(m_L, v)) + return *this; + + lua_rawset(m_L, -3); + } - lua_rawset(m_L, -3); return *this; } @@ -821,8 +881,17 @@ class LuaRef : public LuaRefBase #endif lua_rawgeti(L, LUA_REGISTRYINDEX, m_tableRef); - lua_rawgeti(L, LUA_REGISTRYINDEX, m_keyRef); - lua_gettable(L, -2); + + if (m_keyLiteral != nullptr) + { + lua_getfield(L, -1, m_keyLiteral); + } + else + { + lua_rawgeti(L, LUA_REGISTRYINDEX, m_keyRef); + lua_gettable(L, -2); + } + lua_remove(L, -2); // remove the table } @@ -841,7 +910,30 @@ class LuaRef : public LuaRefBase template TableItem operator[](const T& key) const { - return LuaRef(*this)[key]; + const StackRestore stackRestore(m_L); + + push(m_L); + + if (! Stack::push(m_L, key)) + lua_pushnil(m_L); + + lua_pushvalue(m_L, -2); + const auto tableRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + lua_remove(m_L, -2); + + return TableItem(m_L, tableRef, AdoptTableRef{}); + } + + template + TableItem operator[](const char (&key)[N]) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + + const auto tableRef = luaL_ref(m_L, LUA_REGISTRYINDEX); + + return TableItem(m_L, tableRef, AdoptTableRef{}, key); } //========================================================================================= @@ -862,9 +954,58 @@ class LuaRef : public LuaRefBase return LuaRef(*this).rawget(key); } + //========================================================================================= + /** + * @brief Get a field from the table item value without metamethods in an unsafe fast path. + * + * @param key A field key. + * + * @returns The converted value. + */ + template + T unsafeRawgetField(const char* key) const + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 3, detail::error_lua_stack_overflow); +#endif + + push(m_L); + lua_pushstring(m_L, key); + lua_rawget(m_L, -2); + + auto result = Stack::get(m_L, -1); + lua_pop(m_L, 2); + + return result.value(); + } + + //========================================================================================= + /** + * @brief Set a field on the table item value without metamethods in an unsafe fast path. + * + * @param key A field key. + * @param value A value to assign. + */ + template + void unsafeRawsetField(const char* key, T&& value) const + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 3, detail::error_lua_stack_overflow); +#endif + + push(m_L); + lua_pushstring(m_L, key); + [[maybe_unused]] const auto pushed = Stack>::push(m_L, std::forward(value)); + LUABRIDGE_ASSERT(static_cast(pushed)); + + lua_rawset(m_L, -3); + lua_pop(m_L, 1); + } + private: int m_tableRef = LUA_NOREF; int m_keyRef = LUA_NOREF; + const char* m_keyLiteral = nullptr; }; friend struct Stack; @@ -1252,6 +1393,12 @@ class LuaRef : public LuaRefBase return TableItem(m_L, m_ref); } + template + TableItem operator[](const char (&key)[N]) const + { + return TableItem(m_L, m_ref, key); + } + //============================================================================================= /** * @brief Access a table value using a key. @@ -1276,6 +1423,147 @@ class LuaRef : public LuaRefBase return LuaRef(m_L, FromStack()); } + + //============================================================================================= + /** + * @brief Get a table field by C-string key and convert to T. + * + * This invokes metamethods. + * + * @param key A field key. + * + * @returns A converted value or an error. + */ + template + TypeResult getField(const char* key) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + lua_getfield(m_L, -1, key); + + return Stack::get(m_L, -1); + } + + //============================================================================================= + /** + * @brief Set a table field by C-string key. + * + * This invokes metamethods. + * + * @param key A field key. + * @param value A value to assign. + * + * @returns True if value push succeeded, false otherwise. + */ + template + bool setField(const char* key, T&& value) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + + if (! Stack>::push(m_L, std::forward(value))) + return false; + + lua_setfield(m_L, -2, key); + return true; + } + + //============================================================================================= + /** + * @brief Get a table field by C-string key without metamethods. + * + * @param key A field key. + * + * @returns A converted value or an error. + */ + template + TypeResult rawgetField(const char* key) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + lua_pushstring(m_L, key); + lua_rawget(m_L, -2); + + return Stack::get(m_L, -1); + } + + //============================================================================================= + /** + * @brief Set a table field by C-string key without metamethods. + * + * @param key A field key. + * @param value A value to assign. + * + * @returns True if key/value push succeeded, false otherwise. + */ + template + bool rawsetField(const char* key, T&& value) const + { + const StackRestore stackRestore(m_L); + + push(m_L); + lua_pushstring(m_L, key); + + if (! Stack>::push(m_L, std::forward(value))) + return false; + + lua_rawset(m_L, -3); + return true; + } + + //============================================================================================= + /** + * @brief Get a table field by C-string key without metamethods in an unsafe fast path. + * + * This helper is intended for hot loops where stack conversion is known to succeed. + * + * @param key A field key. + * + * @returns The converted value. + */ + template + T unsafeRawgetField(const char* key) const + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 3, detail::error_lua_stack_overflow); +#endif + + push(m_L); + lua_pushstring(m_L, key); + lua_rawget(m_L, -2); + + auto result = Stack::get(m_L, -1); + lua_pop(m_L, 2); + + return result.value(); + } + + //============================================================================================= + /** + * @brief Set a table field by C-string key without metamethods in an unsafe fast path. + * + * @param key A field key. + * @param value A value to assign. + */ + template + void unsafeRawsetField(const char* key, T&& value) const + { +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(m_L, 3, detail::error_lua_stack_overflow); +#endif + + push(m_L); + lua_pushstring(m_L, key); + [[maybe_unused]] const auto pushed = Stack>::push(m_L, std::forward(value)); + LUABRIDGE_ASSERT(static_cast(pushed)); + + lua_rawset(m_L, -3); + lua_pop(m_L, 1); + } + //============================================================================================= /** * @brief Get the unique hash of a LuaRef. diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 603fe25b..84c54b91 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -165,10 +165,13 @@ class Namespace : public detail::Registrar if (simple) { - lua_pushcfunction_x(L, &detail::index_metamethod_simple, "__index"); + lua_rawgetp_x(L, tableIndex, detail::getPropgetKey()); // Stack: ..., pg | nil + lua_pushvalue(L, tableIndex); // Stack: ..., pg | nil, mt + lua_pushcclosure_x(L, &detail::index_metamethod_simple, "__index", 2); rawsetfield(L, tableIndex, "__index"); - lua_pushcfunction_x(L, &detail::newindex_metamethod_simple, "__newindex"); + lua_rawgetp_x(L, tableIndex, detail::getPropsetKey()); // Stack: ..., ps | nil + lua_pushcclosure_x(L, &detail::newindex_metamethod_simple, "__newindex", 1); rawsetfield(L, tableIndex, "__newindex"); } else @@ -187,10 +190,14 @@ class Namespace : public detail::Registrar if (simple) { - lua_pushcfunction_x(L, &detail::index_metamethod_simple, "__index"); + lua_rawgetp_x(L, tableIndex, detail::getPropgetKey()); // Stack: ..., pg | nil + lua_rawgetp_x(L, tableIndex, detail::getClassKey()); // Stack: ..., pg | nil, cl | nil + lua_pushvalue(L, tableIndex); // Stack: ..., pg | nil, cl | nil, mt + lua_pushcclosure_x(L, &detail::index_metamethod_simple, "__index", 3); rawsetfield(L, tableIndex, "__index"); - lua_pushcfunction_x(L, &detail::newindex_metamethod_simple, "__newindex"); + lua_rawgetp_x(L, tableIndex, detail::getPropsetKey()); // Stack: ..., ps | nil + lua_pushcclosure_x(L, &detail::newindex_metamethod_simple, "__newindex", 1); rawsetfield(L, tableIndex, "__newindex"); } else @@ -225,11 +232,11 @@ class Namespace : public detail::Registrar lua_pushstring(L, type_name.c_str()); // Stack: ns, co, name lua_rawsetp_x(L, -2, detail::getTypeKey()); // co [typeKey] = name. Stack: ns, co - setObjectMetaMethods(-1, ! options.test(extensibleClass)); // Stack: ns, co - - lua_newtable(L); // Stack: ns, co, tb + lua_newtable(L); // Stack: ns, co, propget table (pg) lua_rawsetp_x(L, -2, detail::getPropgetKey()); // Stack: ns, co + setObjectMetaMethods(-1, ! options.test(extensibleClass)); // Stack: ns, co + if (! options.test(visibleMetatables)) { lua_pushboolean(L, 0); @@ -260,6 +267,9 @@ class Namespace : public detail::Registrar lua_pushvalue(L, -1); // Stack: ns, co, cl, cl lua_rawsetp_x(L, -3, detail::getClassKey()); // co [classKey] = cl. Stack: ns, co, cl + + if (! options.test(extensibleClass)) + setObjectMetaMethods(-1, true); } //========================================================================================= @@ -282,8 +292,6 @@ class Namespace : public detail::Registrar pushunsigned(L, options.toUnderlying()); // Stack: ns, co, cl, st, mt, options lua_rawsetp_x(L, -2, detail::getClassOptionsKey()); // st [classOptionsKey] = options. Stack: ns, co, cl, st, mt - setStaticMetaMethods(-1, ! options.test(extensibleClass)); - lua_newtable(L); // Stack: ns, co, cl, st, proget table (pg) lua_rawsetp_x(L, -2, detail::getPropgetKey()); // st [propgetKey] = pg. Stack: ns, co, cl, st @@ -293,6 +301,8 @@ class Namespace : public detail::Registrar lua_pushvalue(L, -2); // Stack: ns, co, cl, st, cl lua_rawsetp_x(L, -2, detail::getClassKey()); // st [classKey] = cl. Stack: ns, co, cl, st + setStaticMetaMethods(-1, ! options.test(extensibleClass)); + if (! options.test(visibleMetatables)) { lua_pushboolean(L, 0); From 881346bf0438ee8e0e48ed91b286f46457384e61 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 18:16:46 +0200 Subject: [PATCH 04/10] More tests --- Benchmarks/CMakeLists.txt | 35 +++++++------- Tests/Source/ClassTests.cpp | 40 ++++++++++++++++ Tests/Source/NamespaceTests.cpp | 31 +++++++++++++ Tests/Source/Tests.cpp | 81 +++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 16 deletions(-) diff --git a/Benchmarks/CMakeLists.txt b/Benchmarks/CMakeLists.txt index 53246902..0d2bc75c 100644 --- a/Benchmarks/CMakeLists.txt +++ b/Benchmarks/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.10) include(FetchContent) +set(LUABRIDGE_BENCHMARK_WITH_SOL2 OFF CACHE BOOL "Whether to benchmark against sol2") +set(LUABRIDGE_SOL2_GIT_REPOSITORY "https://github.com/ThePhD/sol2.git" CACHE STRING "sol2 repository URL") +set(LUABRIDGE_SOL2_GIT_TAG "v3.3.0" CACHE STRING "sol2 git tag or commit") + set(LUABRIDGE_BENCHMARK_SOURCES Benchmark.cpp ../Tests/Lua/LuaLibrary5.4.8.cpp) @@ -18,19 +22,18 @@ target_compile_definitions(LuaBridgeBenchmarks PRIVATE LUABRIDGE_BENCHMARK_LUA54=1 LUABRIDGE_TEST_LUA_VERSION=504) -set(LUABRIDGE_SOL2_GIT_REPOSITORY "https://github.com/ThePhD/sol2.git" CACHE STRING "sol2 repository URL") -set(LUABRIDGE_SOL2_GIT_TAG "v3.3.0" CACHE STRING "sol2 git tag or commit") - -FetchContent_Declare( - sol2 - GIT_REPOSITORY ${LUABRIDGE_SOL2_GIT_REPOSITORY} - GIT_TAG ${LUABRIDGE_SOL2_GIT_TAG}) -FetchContent_MakeAvailable(sol2) - -target_include_directories(LuaBridgeBenchmarks PRIVATE - ${sol2_SOURCE_DIR}/include) - -target_compile_definitions(LuaBridgeBenchmarks PRIVATE - LUABRIDGE_BENCHMARK_WITH_SOL2=1 - SOL_ALL_SAFETIES_ON=1 - SOL_LUA_VERSION=504) +if (LUABRIDGE_BENCHMARK_WITH_SOL2) + FetchContent_Declare( + sol2 + GIT_REPOSITORY ${LUABRIDGE_SOL2_GIT_REPOSITORY} + GIT_TAG ${LUABRIDGE_SOL2_GIT_TAG}) + FetchContent_MakeAvailable(sol2) + + target_include_directories(LuaBridgeBenchmarks PRIVATE + ${sol2_SOURCE_DIR}/include) + + target_compile_definitions(LuaBridgeBenchmarks PRIVATE + LUABRIDGE_BENCHMARK_WITH_SOL2=1 + SOL_ALL_SAFETIES_ON=1 + SOL_LUA_VERSION=504) +endif() diff --git a/Tests/Source/ClassTests.cpp b/Tests/Source/ClassTests.cpp index 786f763e..5997f7d4 100644 --- a/Tests/Source/ClassTests.cpp +++ b/Tests/Source/ClassTests.cpp @@ -2494,6 +2494,46 @@ TEST_F(ClassMetaMethods, MetamethodsShouldNotBePartOfClassInstances) EXPECT_TRUE(result().isFunction()); } +TEST_F(ClassMetaMethods, MetamethodsShouldNotBePartOfClassStaticTable) +{ + using Int = Class; + + int value = 10; + + luabridge::getGlobalNamespace(L) + .beginClass("Int") + .addConstructor() + .addStaticFunction("inc", +[](int x) { return x + 1; }) + .addStaticProperty("rw", [&value] { return value; }, [&value](int x) { value = x; }) + .addStaticProperty("ro", [&value] { return value; }) + .endClass(); + + runLua("result = Int.inc(41)"); + ASSERT_TRUE(result().isNumber()); + ASSERT_EQ(42, result()); + + runLua("result = Int.__index"); + ASSERT_TRUE(result().isNil()); + + runLua("result = Int.__newindex"); + ASSERT_TRUE(result().isNil()); + + runLua("Int.rw = 77"); + ASSERT_EQ(77, value); + + runLua("result = Int.rw"); + ASSERT_TRUE(result().isNumber()); + ASSERT_EQ(77, result()); + +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("Int.ro = 1"), std::exception); + ASSERT_THROW(runLua("Int.missing = 1"), std::exception); +#else + ASSERT_FALSE(runLua("Int.ro = 1")); + ASSERT_FALSE(runLua("Int.missing = 1")); +#endif +} + TEST_F(ClassMetaMethods, MetamethodsShouldNotBeWritable) { using Int = Class; diff --git a/Tests/Source/NamespaceTests.cpp b/Tests/Source/NamespaceTests.cpp index 6a13df03..6c4d60bc 100644 --- a/Tests/Source/NamespaceTests.cpp +++ b/Tests/Source/NamespaceTests.cpp @@ -458,6 +458,37 @@ TEST_F(NamespaceTests, Properties_ProxyCFunctions_ReadOnly) ASSERT_EQ(3, result()); } +TEST_F(NamespaceTests, Properties_SimpleMetaMethods) +{ + int value = 5; + + luabridge::getGlobalNamespace(L) + .beginNamespace("ns") + .addFunction("inc", +[](int x) { return x + 1; }) + .addProperty("rw", [&value] { return value; }, [&value](int v) { value = v; }) + .addProperty("ro", [&value] { return value; }) + .endNamespace(); + + runLua("result = ns.inc(41)"); + ASSERT_TRUE(result().isNumber()); + ASSERT_EQ(42, result()); + + runLua("ns.rw = 9"); + ASSERT_EQ(9, value); + + runLua("result = ns.rw"); + ASSERT_TRUE(result().isNumber()); + ASSERT_EQ(9, result()); + +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("ns.ro = 1"), std::exception); + ASSERT_THROW(runLua("ns.missing = 1"), std::exception); +#else + ASSERT_FALSE(runLua("ns.ro = 1")); + ASSERT_FALSE(runLua("ns.missing = 1")); +#endif +} + namespace { struct Class {}; } // namespace diff --git a/Tests/Source/Tests.cpp b/Tests/Source/Tests.cpp index 9003e359..1fc47243 100644 --- a/Tests/Source/Tests.cpp +++ b/Tests/Source/Tests.cpp @@ -436,6 +436,87 @@ TEST_F(LuaBridgeTest, CallReturnTypeResult) #endif } +TEST_F(LuaBridgeTest, CallReturnTypeResultErrorPaths) +{ + runLua("function ret0() end"); + runLua("function ret1() return 11 end"); + runLua("function ret2bad() return 10, 'bad' end"); + + { + auto f = luabridge::getGlobal(L, "ret0"); + const int stackTop = lua_gettop(L); + + auto result = f.call(); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTypeCast), result.error()); + EXPECT_EQ(stackTop, lua_gettop(L)); + } + + { + auto f = luabridge::getGlobal(L, "ret1"); + const int stackTop = lua_gettop(L); + + auto result = f.call(); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTableSizeInCast), result.error()); + EXPECT_EQ(stackTop, lua_gettop(L)); + } + + { + auto f = luabridge::getGlobal(L, "ret1"); + const int stackTop = lua_gettop(L); + + auto result = f.call>(); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTableSizeInCast), result.error()); + EXPECT_EQ(stackTop, lua_gettop(L)); + } + + { + auto f = luabridge::getGlobal(L, "ret2bad"); + const int stackTop = lua_gettop(L); + + auto result = f.call>(); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTypeCast), result.error()); + EXPECT_EQ(stackTop, lua_gettop(L)); + } +} + +TEST_F(LuaBridgeTest, CallWithHandlerTypedReturnAndStackRestore) +{ + runLua("function fOk(x) return x + 1 end"); + runLua("function fFail() error('boom') end"); + + auto fOk = luabridge::getGlobal(L, "fOk"); + auto fFail = luabridge::getGlobal(L, "fFail"); + + int handlerCalls = 0; + auto handler = [&handlerCalls](lua_State*) -> int + { + ++handlerCalls; + return 0; + }; + + { + const int stackTop = lua_gettop(L); + auto result = fOk.callWithHandler(handler, 41); + ASSERT_TRUE(result); + EXPECT_EQ(42, *result); + EXPECT_EQ(0, handlerCalls); + EXPECT_EQ(stackTop, lua_gettop(L)); + } + + { + const int stackTop = lua_gettop(L); + auto result = fFail.callWithHandler(handler); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::LuaFunctionCallFailed), result.error()); + EXPECT_EQ(1, handlerCalls); + EXPECT_EQ(stackTop, lua_gettop(L)); + } +} + TEST_F(LuaBridgeTest, InvokePassingUnregisteredClassShouldThrowAndRestoreStack) { class Unregistered {} unregistered; From d27b209b3a6412a85b9c9aedef05ed360d5a9c6e Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 18:25:08 +0200 Subject: [PATCH 05/10] Fix missing lua55 coverage in coveralls --- .github/workflows/coverage.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 59fce3d8..d7f9f7ac 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -408,7 +408,7 @@ jobs: coveralls: runs-on: ubuntu-latest - needs: [lua51, lua52, lua53, lua54, luajit, luau, ravi] + needs: [lua51, lua52, lua53, lua54, lua55, luajit, luau, ravi] steps: - uses: actions/checkout@v6 with: @@ -446,6 +446,12 @@ jobs: path: "${{runner.workspace}}/build/coverage/*.info" key: lcov-lua54-${{runner.os}}-${{github.sha}} + - name: Restore Lcov Files Lua 5.5 + uses: actions/cache@v5 + with: + path: "${{runner.workspace}}/build/coverage/*.info" + key: lcov-lua55-${{runner.os}}-${{github.sha}} + - name: Restore Lcov Files LuaJIT uses: actions/cache@v5 with: @@ -472,6 +478,7 @@ jobs: -a "coverage/lua52.info" \ -a "coverage/lua53.info" \ -a "coverage/lua54.info" \ + -a "coverage/lua55.info" \ -a "coverage/luajit.info" \ -a "coverage/luau.info" \ -a "coverage/ravi.info" \ From cb9af5dfdd2182c2e26ca811ca611d911695a8ad Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 18:31:44 +0200 Subject: [PATCH 06/10] More markdown updates --- CHANGES.md | 1 + Manual.md | 2 +- README.md | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f369484a..a47cd16f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * Moved to C++17 as minimum supported standard C++ version. * Reworked the whole library to be able to use it without c++ exceptions enabled. +* Lot of optimisations have been added, and now the library is *fast*, nearly in the same league as sol2 v3.x. * Breaking Change: The method `Stack::push` now takes a `std::error_code&` as last parameter and returns a `bool`. * Breaking Change: The class `LuaException` has been reworked and it now take a `std::error_code` instead of a int. * Breaking Change: The class `LuaException` is now thrown if a unregistered class is pushed via the Stack class, also when calling `LuaRef::operator()`, but only if exceptions are enabled. diff --git a/Manual.md b/Manual.md index 1e311ee2..d1878cc6 100644 --- a/Manual.md +++ b/Manual.md @@ -118,7 +118,7 @@ To expose Lua objects to C++, a class called `luabridge::LuaRef` is provided. Th LuaBridge tries to be efficient as possible when creating the "glue" that exposes C++ data and functions to Lua. At the same time, the code was written with the intention that it is all as simple and clear as possible, without resorting to obscure C++ idioms, ugly preprocessor macros, or configuration settings. Furthermore, it is designed to be "header-only", making it very easy to integrate into your projects. -Because LuaBridge was written with simplicity in mind there are some features that are not available. Although it comes close to the highest possible performance, LuaBridge is not quite the fastest, [OOLua](http://code.google.com/p/oolua/) and [sol2](https://github.com/ThePhD/sol2) outperforms LuaBridge in some tests, but they are also bigger and slower to compile. While being powerful, LuaBridge is pretty compact and simpler to understand and debug, and also does not try to implement every possible feature: [LuaBind](http://www.rasterbar.com/products/luabind.html) (requires Boost) and [sol2](https://github.com/ThePhD/sol2) explores every corner of the C++ language. +Because LuaBridge was written with simplicity in mind there are some features that are not available. LuaBridge3 has been extensively optimized and now competes directly with [sol2](https://github.com/ThePhD/sol2) — one of the fastest C++/Lua binding libraries — across most workloads. In fact, LuaBridge3 outperforms sol2 in certain scenarios such as member function calls from Lua and global table writes, while remaining more compact and faster to compile. While sol2 has an edge in chained table access, the overall performance gap is negligible for most use cases. LuaBridge3 also does not try to implement every possible feature: [LuaBind](http://www.rasterbar.com/products/luabind.html) (requires Boost) and [sol2](https://github.com/ThePhD/sol2) explore every corner of the C++ language. LuaBridge does not support: diff --git a/README.md b/README.md index d05c90a8..dc83e6f5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,30 @@ LuaBridge3 is usable from a compliant C++17 compiler and offers the following fe * Expose C++ classes allowing them to use the flexibility of lua property lookup. * Interoperable with most common c++ standard library container types. * Written in a clear and easy to debug style. +* Competitive performance with the fastest C++/Lua binding libraries available. + +## Performance + +LuaBridge3 has been heavily optimized and now competes directly with [sol2](https://github.com/ThePhD/sol2) — one of the fastest C++/Lua binding libraries — across most workloads. In some cases (e.g. member function calls from Lua) LuaBridge3 is actually **faster** than sol2: + +| Case | LuaBridge3 (ns/op) | sol2 (ns/op) | +|------------------------------|--------------------|--------------| +| lua_empty_loop | 2.10 | 2.07 | +| lua_to_cpp_free_fn | 28.30 | 21.54 | +| lua_to_cpp_member | **70.23** | 101.13 | +| lua_to_cpp_property | 139.29 | 125.34 | +| lua_to_cpp_property_set | 70.70 | 62.58 | +| lua_to_cpp_property_get | 64.65 | 61.98 | +| cpp_table_global_get | 15.24 | 12.60 | +| cpp_table_global_set | **9.99** | 11.31 | +| cpp_table_get | 26.24 | 18.54 | +| cpp_table_set | 23.41 | 17.74 | +| cpp_table_chained_get | 70.55 | 27.87 | +| cpp_table_chained_set | 66.87 | 26.50 | +| cpp_to_lua_call | 33.41 | 30.92 | +| cpp_c_function_through_lua | 52.52 | 46.47 | + +Bold entries indicate cases where LuaBridge3 outperforms sol2. Lower is better (nanoseconds per operation). ## Improvements Over Vanilla LuaBridge From 84f966f9346f2540b55aa8c21a327ce12303f581 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 18:43:37 +0200 Subject: [PATCH 07/10] More test exercising --- Tests/Source/Tests.cpp | 187 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/Tests/Source/Tests.cpp b/Tests/Source/Tests.cpp index 1fc47243..8d2d56b3 100644 --- a/Tests/Source/Tests.cpp +++ b/Tests/Source/Tests.cpp @@ -25,6 +25,26 @@ T identityCFunction(T value) { return value; } + +int testGetter777(lua_State* L) +{ + lua_pushinteger(L, 777); + return 1; +} + +int testSetValueWithSelf(lua_State* L) +{ + lua_pushvalue(L, 2); + lua_setglobal(L, "captured"); + return 0; +} + +int testSetValueStatic(lua_State* L) +{ + lua_pushvalue(L, 1); + lua_setglobal(L, "captured"); + return 0; +} } // namespace struct LuaBridgeTest : TestBase @@ -517,6 +537,173 @@ TEST_F(LuaBridgeTest, CallWithHandlerTypedReturnAndStackRestore) } } +TEST_F(LuaBridgeTest, IndexMetamethodSimple_ObjectFallbackBranches) +{ + // Exercise the table-based fallback branch of index_metamethod_simple. + auto callIndex = [this](const char* key) -> bool + { + lua_newtable(L); // self + + lua_newtable(L); // mt + lua_newtable(L); // propget + luabridge::lua_pushcfunction_x(L, &testGetter777, "testGetter777"); + lua_setfield(L, -2, "viaGetter"); + luabridge::lua_rawsetp_x(L, -2, luabridge::detail::getPropgetKey()); + lua_setmetatable(L, -2); + + lua_newtable(L); // upvalue #1 (pg) + lua_newtable(L); // upvalue #2 (mt) + luabridge::lua_pushcclosure_x(L, &luabridge::detail::index_metamethod_simple, "index_metamethod_simple_object", 2); + + lua_pushvalue(L, -2); // self + lua_pushstring(L, key); + const int code = lua_pcall(L, 2, 1, 0); + + lua_remove(L, -2); // remove self, keep result or error + return code == LUABRIDGE_LUA_OK; + }; + + // 1) Value found in self table branch. + auto tmpSelf = luabridge::newTable(L); + tmpSelf["selfField"] = 55; + luabridge::setGlobal(L, tmpSelf, "tmpSelf"); + + lua_getglobal(L, "tmpSelf"); // self + lua_newtable(L); // mt + lua_newtable(L); // propget + luabridge::lua_rawsetp_x(L, -2, luabridge::detail::getPropgetKey()); + lua_setmetatable(L, -2); + lua_newtable(L); + lua_newtable(L); + luabridge::lua_pushcclosure_x(L, &luabridge::detail::index_metamethod_simple, "index_metamethod_simple_object", 2); + lua_pushvalue(L, -2); + lua_pushstring(L, "selfField"); + ASSERT_EQ(LUABRIDGE_LUA_OK, lua_pcall(L, 2, 1, 0)); + ASSERT_TRUE(lua_isnumber(L, -1)); + ASSERT_EQ(55, static_cast(lua_tointeger(L, -1))); + lua_pop(L, 2); + + // 2) Value found in metatable branch. + lua_newtable(L); // self + lua_newtable(L); // mt + lua_pushinteger(L, 66); + lua_setfield(L, -2, "metaField"); + lua_newtable(L); + luabridge::lua_rawsetp_x(L, -2, luabridge::detail::getPropgetKey()); + lua_setmetatable(L, -2); + lua_newtable(L); + lua_newtable(L); + luabridge::lua_pushcclosure_x(L, &luabridge::detail::index_metamethod_simple, "index_metamethod_simple_object", 2); + lua_pushvalue(L, -2); + lua_pushstring(L, "metaField"); + ASSERT_EQ(LUABRIDGE_LUA_OK, lua_pcall(L, 2, 1, 0)); + ASSERT_TRUE(lua_isnumber(L, -1)); + ASSERT_EQ(66, static_cast(lua_tointeger(L, -1))); + lua_pop(L, 2); + + // 3) Value resolved via propget callable branch. + ASSERT_TRUE(callIndex("viaGetter")); + ASSERT_TRUE(lua_isnumber(L, -1)); + ASSERT_EQ(777, static_cast(lua_tointeger(L, -1))); + lua_pop(L, 1); + + // 4) Unknown key returns nil at final fallback. + ASSERT_TRUE(callIndex("doesNotExist")); + ASSERT_TRUE(lua_isnil(L, -1)); + lua_pop(L, 1); +} + +TEST_F(LuaBridgeTest, NewIndexMetamethodSimple_FallbackBranches) +{ + // Object variant fallback path (self is table). + { + lua_newtable(L); // self + lua_newtable(L); // mt + lua_newtable(L); // propset + luabridge::lua_pushcfunction_x(L, &testSetValueWithSelf, "testSetValueWithSelf"); + lua_setfield(L, -2, "x"); + luabridge::lua_rawsetp_x(L, -2, luabridge::detail::getPropsetKey()); + lua_setmetatable(L, -2); + + lua_newtable(L); // upvalue #1 + luabridge::lua_pushcclosure_x(L, &luabridge::detail::newindex_metamethod_simple, "newindex_metamethod_simple_object", 1); + lua_pushvalue(L, -2); + lua_pushstring(L, "x"); + lua_pushinteger(L, 123); + ASSERT_EQ(LUABRIDGE_LUA_OK, lua_pcall(L, 3, 0, 0)); + lua_pop(L, 1); + + auto captured = luabridge::getGlobal(L, "captured"); + ASSERT_TRUE(captured.isNumber()); + ASSERT_EQ(123, captured.unsafe_cast()); + } + + // Object variant fallback error when propset table exists but key is missing. + { + lua_newtable(L); // self + lua_newtable(L); // mt + lua_newtable(L); // propset + luabridge::lua_rawsetp_x(L, -2, luabridge::detail::getPropsetKey()); + lua_setmetatable(L, -2); + + lua_newtable(L); // upvalue #1 + luabridge::lua_pushcclosure_x(L, &luabridge::detail::newindex_metamethod_simple, "newindex_metamethod_simple_object", 1); + lua_pushvalue(L, -2); + lua_pushstring(L, "missing"); + lua_pushinteger(L, 1); + ASSERT_NE(LUABRIDGE_LUA_OK, lua_pcall(L, 3, 0, 0)); + auto err = lua_tostring(L, -1); + ASSERT_NE(nullptr, err); + EXPECT_TRUE(std::string(err).find("no writable member 'missing'") != std::string::npos); + lua_pop(L, 2); + } + + // Static variant fallback path (self is userdata to bypass simple table fast path). + { + lua_newuserdata(L, 1); // self + lua_newtable(L); // mt + lua_newtable(L); // propset + luabridge::lua_pushcfunction_x(L, &testSetValueStatic, "testSetValueStatic"); + lua_setfield(L, -2, "x"); + luabridge::lua_rawsetp_x(L, -2, luabridge::detail::getPropsetKey()); + lua_setmetatable(L, -2); + + lua_newtable(L); // upvalue #1 + luabridge::lua_pushcclosure_x(L, &luabridge::detail::newindex_metamethod_simple, "newindex_metamethod_simple_static", 1); + lua_pushvalue(L, -2); + lua_pushstring(L, "x"); + lua_pushinteger(L, 321); + ASSERT_EQ(LUABRIDGE_LUA_OK, lua_pcall(L, 3, 0, 0)); + lua_pop(L, 1); + + auto captured = luabridge::getGlobal(L, "captured"); + ASSERT_TRUE(captured.isNumber()); + ASSERT_EQ(321, captured.unsafe_cast()); + } +} + +TEST_F(LuaBridgeTest, ReadOnlyErrorAndArgumentDecodeRaiseLuaError) +{ + // Explicitly exercise read_only_error -> raise_lua_error path. + lua_pushstring(L, "locked"); + luabridge::lua_pushcclosure_x(L, &luabridge::detail::read_only_error, "read_only_error", 1); + ASSERT_NE(LUABRIDGE_LUA_OK, lua_pcall(L, 0, 0, 0)); + { + auto err = lua_tostring(L, -1); + ASSERT_NE(nullptr, err); + EXPECT_TRUE(std::string(err).find("'locked' is read-only") != std::string::npos); + } + lua_pop(L, 1); + + // Exercise argument decode error path in invocation wrappers (raise_lua_error at decode). + luabridge::getGlobalNamespace(L) + .addFunction("expectInt", +[](int) { return 0; }); + + auto [ok, err] = runLuaCaptureError("expectInt('not an int')"); + EXPECT_FALSE(ok); + EXPECT_TRUE(err.find("Error decoding argument #1") != std::string::npos); +} + TEST_F(LuaBridgeTest, InvokePassingUnregisteredClassShouldThrowAndRestoreStack) { class Unregistered {} unregistered; From b9acd94d2a30c428e21263cf452a6fb65285f966 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 21:45:16 +0200 Subject: [PATCH 08/10] More tests --- Source/LuaBridge/detail/CFunctions.h | 44 +++--- Source/LuaBridge/detail/Userdata.h | 196 ++++++++++++++++--------- Tests/Lua/LuaJIT.2.1/CMakeLists.txt | 2 +- Tests/Source/ClassTests.cpp | 16 ++- Tests/Source/LuaRefTests.cpp | 206 +++++++++++++++++++++++++++ Tests/Source/Tests.cpp | 31 ++++ Tests/Source/UserdataTests.cpp | 168 ++++++++++++++++++++++ 7 files changed, 570 insertions(+), 93 deletions(-) diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index 5904db74..ad1517d4 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -1078,7 +1078,9 @@ struct property_getter { static int call(lua_State* L) { - C* c = Userdata::get(L, 1, true); + auto c = Userdata::get(L, 1, true); + if (! c) + raise_lua_error(L, "%s", c.error_cstr()); T C::** mp = static_cast(lua_touserdata(L, lua_upvalueindex(1))); @@ -1088,7 +1090,7 @@ struct property_getter try { #endif - result = Stack::push(L, c->**mp); + result = Stack::push(L, (*c)->**mp); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -1164,7 +1166,9 @@ struct property_setter { static int call(lua_State* L) { - C* c = Userdata::get(L, 1, false); + auto c = Userdata::get(L, 1, false); + if (! c) + raise_lua_error(L, "%s", c.error_cstr()); T C::** mp = static_cast(lua_touserdata(L, lua_upvalueindex(1))); @@ -1176,7 +1180,7 @@ struct property_setter if (! result) raise_lua_error(L, "%s", result.error_cstr()); - c->** mp = std::move(*result); + (*c)->** mp = std::move(*result); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -1360,12 +1364,14 @@ int invoke_member_function(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - T* ptr = Userdata::get(L, 1, false); + auto ptr = Userdata::get(L, 1, false); + if (! ptr) + raise_lua_error(L, "%s", ptr.error_cstr()); const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(1))); LUABRIDGE_ASSERT(func != nullptr); - return function::call(L, ptr, func); + return function::call(L, *ptr, func); } template @@ -1375,12 +1381,14 @@ int invoke_const_member_function(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - const T* ptr = Userdata::get(L, 1, true); + auto ptr = Userdata::get(L, 1, true); + if (! ptr) + raise_lua_error(L, "%s", ptr.error_cstr()); const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(1))); LUABRIDGE_ASSERT(func != nullptr); - return function::call(L, ptr, func); + return function::call(L, *ptr, func); } //================================================================================================= @@ -1396,7 +1404,9 @@ int invoke_member_cfunction(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - T* t = Userdata::get(L, 1, false); + auto t = Userdata::get(L, 1, false); + if (! t) + raise_lua_error(L, "%s", t.error_cstr()); const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(1))); LUABRIDGE_ASSERT(func != nullptr); @@ -1405,7 +1415,7 @@ int invoke_member_cfunction(lua_State* L) try { #endif - return (t->*func)(L); + return ((*t)->*func)(L); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -1425,7 +1435,9 @@ int invoke_const_member_cfunction(lua_State* L) LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); - const T* t = Userdata::get(L, 1, true); + auto t = Userdata::get(L, 1, true); + if (! t) + raise_lua_error(L, "%s", t.error_cstr()); const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(1))); LUABRIDGE_ASSERT(func != nullptr); @@ -1434,7 +1446,7 @@ int invoke_const_member_cfunction(lua_State* L) try { #endif - return (t->*func)(L); + return ((*t)->*func)(L); #if LUABRIDGE_HAS_EXCEPTIONS } @@ -2385,11 +2397,11 @@ struct destructor_forwarder void operator()(lua_State* L) { - auto* value = Userdata::get(L, -1, false); - if (value == nullptr) - raise_lua_error(L, "invalid object destruction"); + auto value = Userdata::get(L, -1, false); + if (! value) + raise_lua_error(L, "%s", value.error_cstr()); - std::invoke(m_func, value); + std::invoke(m_func, *value); } private: diff --git a/Source/LuaBridge/detail/Userdata.h b/Source/LuaBridge/detail/Userdata.h index cd366cd8..7f4d4331 100644 --- a/Source/LuaBridge/detail/Userdata.h +++ b/Source/LuaBridge/detail/Userdata.h @@ -20,6 +20,26 @@ namespace luabridge { namespace detail { +template +std::error_code getNilBadArgError(lua_State* L, int index) +{ + const std::string message = std::string(typeName()) + " expected, got no value"; + +#if LUABRIDGE_HAS_EXCEPTIONS + if (index > 0 && LuaException::areExceptionsEnabled(L)) + { + lua_pushstring(L, message.c_str()); + LuaException::raise(L, makeErrorCode(ErrorCode::InvalidTypeCast)); + } +#endif + + // Lua-call argument path: keep detailed bad-argument text when exceptions are disabled. + if (index > 0) + luaL_argerror(L, lua_absindex(L, index), message.c_str()); + + return makeErrorCode(ErrorCode::InvalidTypeCast); +} + //================================================================================================= /** * @brief Return the identity pointer for our lightuserdata tokens. @@ -61,17 +81,80 @@ class Userdata * The Userdata must be derived from or the same as the given base class, identified by the key. If canBeConst is false, generates * an error if the resulting Userdata represents to a const object. We do the type check first so that the error message is informative. */ - static Userdata* getClass(lua_State* L, - int index, - const void* registryConstKey, - const void* registryClassKey, - bool canBeConst) + static std::error_code getClassErrorCode(lua_State* L, const void* registryClassKey) + { + lua_rawgetp_x(L, LUA_REGISTRYINDEX, registryClassKey); + const bool classIsRegistered = lua_istable(L, -1); + lua_pop(L, 1); + + return makeErrorCode(classIsRegistered ? ErrorCode::InvalidTypeCast : ErrorCode::ClassNotRegistered); + } + + static std::error_code getBadArgError(lua_State* L, int index, const void* registryClassKey) + { + const int absIndex = lua_absindex(L, index); + + lua_rawgetp_x(L, LUA_REGISTRYINDEX, registryClassKey); // Stack: registry metatable (rt) | nil + const bool classIsRegistered = lua_istable(L, -1); + + const char* expected = "unregistered class"; + if (classIsRegistered) + { + lua_rawgetp_x(L, -1, getTypeKey()); // Stack: rt, registry type + if (lua_isstring(L, -1)) + expected = lua_tostring(L, -1); + lua_pop(L, 1); // Stack: rt + } + + const char* got = nullptr; + if (lua_isuserdata(L, absIndex)) + { + lua_getmetatable(L, absIndex); // Stack: rt | nil, ot | nil + if (lua_istable(L, -1)) + { + lua_rawgetp_x(L, -1, getTypeKey()); // Stack: rt | nil, ot, object type | nil + if (lua_isstring(L, -1)) + got = lua_tostring(L, -1); + + lua_pop(L, 1); // Stack: rt | nil, ot + } + + lua_pop(L, 1); // Stack: rt | nil + } + + if (! got) + got = lua_typename(L, lua_type(L, absIndex)); + + lua_pop(L, 1); // Stack: - + + const auto errorCode = classIsRegistered ? ErrorCode::InvalidTypeCast : ErrorCode::ClassNotRegistered; + +#if LUABRIDGE_HAS_EXCEPTIONS + if (LuaException::areExceptionsEnabled(L)) + { + const std::string message = std::string(expected) + " expected, got " + got; + lua_pushstring(L, message.c_str()); + + LuaException::raise(L, makeErrorCode(errorCode)); + } +#endif + + return makeErrorCode(errorCode); + } + + static TypeResult getClass(lua_State* L, + int index, + const void* registryConstKey, + const void* registryClassKey, + bool canBeConst) { const int result = lua_getmetatable(L, index); // Stack: object metatable (ot) | nil if (result == 0 || !lua_istable(L, -1)) { - lua_rawgetp_x(L, LUA_REGISTRYINDEX, registryClassKey); // Stack: ot | nil, registry metatable (rt) | nil - return throwBadArg(L, index); + if (result != 0) + lua_pop(L, 1); + + return getBadArgError(L, index, registryClassKey); } lua_rawgetp_x(L, -1, getConstKey()); // Stack: ot | nil, const table (co) | nil @@ -105,8 +188,8 @@ class Userdata if (lua_isnil(L, -1)) // Stack: rt, ot, nil { // Drop the object metatable because it may be some parent metatable - lua_pop(L, 2); // Stack: rt - return throwBadArg(L, index); + lua_pop(L, 3); // Stack: - + return getBadArgError(L, index, registryClassKey); } lua_remove(L, -2); // Stack: rt, pot @@ -154,48 +237,6 @@ class Userdata // no return } - static Userdata* throwBadArg(lua_State* L, int index) - { - LUABRIDGE_ASSERT(lua_istable(L, -1) || lua_isnil(L, -1)); // Stack: rt | nil - - const char* expected = 0; - if (lua_isnil(L, -1)) // Stack: nil - { - expected = "unregistered class"; - } - else - { - lua_rawgetp_x(L, -1, getTypeKey()); // Stack: rt, registry type - expected = lua_tostring(L, -1); - lua_pop(L, 1); // Stack: rt - } - - const char* got = nullptr; - if (lua_isuserdata(L, index)) - { - lua_getmetatable(L, index); // Stack: rt, ot | nil - if (lua_istable(L, -1)) // Stack: rt, ot - { - lua_rawgetp_x(L, -1, getTypeKey()); // Stack: rt, ot, object type | nil - if (lua_isstring(L, -1)) - got = lua_tostring(L, -1); - - lua_pop(L, 1); // Stack: rt, ot - } - - lua_pop(L, 1); // Stack: rt - } - - if (!got) - { - lua_pop(L, 1); // Stack - got = lua_typename(L, lua_type(L, index)); - } - - luaL_argerror(L, index, lua_pushfstring(L, "%s expected, got %s", expected, got)); - return nullptr; - } - public: virtual ~Userdata() {} @@ -233,7 +274,7 @@ class Userdata * @return A pointer if the class and constness match. */ template - static T* get(lua_State* L, int index, bool canBeConst) + static TypeResult get(lua_State* L, int index, bool canBeConst) { if (lua_isnil(L, index)) return nullptr; @@ -259,11 +300,11 @@ class Userdata lua_pop(L, 1); } - auto* clazz = getClass(L, absIndex, constId, classId, canBeConst); + auto clazz = getClass(L, absIndex, constId, classId, canBeConst); if (! clazz) - return nullptr; + return clazz.error(); - return static_cast(clazz->getPointer()); + return static_cast((*clazz)->getPointer()); } template @@ -808,11 +849,11 @@ struct StackHelper { using CastType = std::remove_const_t::Type>; - auto* result = Userdata::get(L, index, true); + auto result = Userdata::get(L, index, true); if (! result) - return makeErrorCode(ErrorCode::InvalidTypeCast); + return result.error(); - return ContainerTraits::construct(result); + return ContainerTraits::construct(*result); } }; @@ -837,11 +878,14 @@ struct StackHelper static TypeResult> get(lua_State* L, int index) { - auto* result = Userdata::get(L, index, true); + auto result = Userdata::get(L, index, true); if (! result) - return makeErrorCode(ErrorCode::InvalidTypeCast); // nil passed to reference + return result.error(); // nil passed to reference - return std::cref(*result); + if (*result == nullptr) + return getNilBadArgError(L, index); + + return std::cref(**result); } }; @@ -865,11 +909,11 @@ struct RefStackHelper static ReturnType get(lua_State* L, int index) { - auto* result = Userdata::get(L, index, true); + auto result = Userdata::get(L, index, true); if (! result) - return makeErrorCode(ErrorCode::InvalidTypeCast); + return result.error(); - return ContainerTraits::construct(result); + return ContainerTraits::construct(*result); } }; @@ -890,11 +934,14 @@ struct RefStackHelper static ReturnType get(lua_State* L, int index) { - auto* result = Userdata::get(L, index, true); + auto result = Userdata::get(L, index, true); if (! result) - return makeErrorCode(ErrorCode::InvalidTypeCast); // nil passed to reference + return result.error(); // nil passed to reference - return std::ref(*result); + if (*result == nullptr) + return getNilBadArgError(L, index); + + return std::ref(**result); } }; @@ -909,11 +956,11 @@ struct UserdataGetter static ReturnType get(lua_State* L, int index) { - auto* result = Userdata::get(L, index, true); + auto result = Userdata::get(L, index, true); if (! result) - return makeErrorCode(ErrorCode::InvalidTypeCast); + return result.error(); - return result; + return *result; } }; @@ -1014,7 +1061,14 @@ struct StackOpSelector static Result push(lua_State* L, const T* value) { return UserdataPtr::push(L, value); } - static ReturnType get(lua_State* L, int index) { return Userdata::get(L, index, true); } + static ReturnType get(lua_State* L, int index) + { + auto result = Userdata::get(L, index, true); + if (! result) + return result.error(); + + return *result; + } template static bool isInstance(lua_State* L, int index) { return Userdata::isInstance(L, index); } diff --git a/Tests/Lua/LuaJIT.2.1/CMakeLists.txt b/Tests/Lua/LuaJIT.2.1/CMakeLists.txt index 2f3bffb5..3748721b 100644 --- a/Tests/Lua/LuaJIT.2.1/CMakeLists.txt +++ b/Tests/Lua/LuaJIT.2.1/CMakeLists.txt @@ -274,7 +274,7 @@ IF(HAS_POSITION_INDEPENDENT_CODE) ENDIF() if ( WIN32 ) - add_definitions ( -DLUAJIT_OS=LUAJIT_OS_WINDOWS ) + add_definitions ( -DLUAJIT_OS=LUAJIT_OS_WINDOWS -D_CRT_SECURE_NO_WARNINGS=1 ) if(NOT MSVC) set ( CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -malign-double" ) endif() diff --git a/Tests/Source/ClassTests.cpp b/Tests/Source/ClassTests.cpp index 5997f7d4..09e4e554 100644 --- a/Tests/Source/ClassTests.cpp +++ b/Tests/Source/ClassTests.cpp @@ -3157,34 +3157,40 @@ TEST_F(ClassTests, WrongThrowBadArgObjectDescription) #endif #else + auto expectBadArgOrInvalidCast = [](const std::string& errorMessage, const char* gotToken) + { + EXPECT_TRUE(errorMessage.find(gotToken) != std::string::npos + || errorMessage.find("can't be cast") != std::string::npos); + }; + { auto [result, errorMessage] = runLuaCaptureError("textSingleXYZ()"); ASSERT_FALSE(result); - EXPECT_NE(std::string::npos, errorMessage.find("got no value")); + expectBadArgOrInvalidCast(errorMessage, "got no value"); } { auto [result, errorMessage] = runLuaCaptureError("textXYZ(1, 1.0)"); ASSERT_FALSE(result); - EXPECT_NE(std::string::npos, errorMessage.find("got no value")); + expectBadArgOrInvalidCast(errorMessage, "got no value"); } { auto [result, errorMessage] = runLuaCaptureError("textXYZ(1, 1.0, 1)"); ASSERT_FALSE(result); - EXPECT_NE(std::string::npos, errorMessage.find("got number")); + expectBadArgOrInvalidCast(errorMessage, "got number"); } { auto [result, errorMessage] = runLuaCaptureError("textXYZ(1, 1.0, '1')"); ASSERT_FALSE(result); - EXPECT_NE(std::string::npos, errorMessage.find("got string")); + expectBadArgOrInvalidCast(errorMessage, "got string"); } { auto [result, errorMessage] = runLuaCaptureError("textXYZ(1, 1.0, ABC())"); ASSERT_FALSE(result); - EXPECT_NE(std::string::npos, errorMessage.find("got ABC")); + expectBadArgOrInvalidCast(errorMessage, "got ABC"); } #endif } diff --git a/Tests/Source/LuaRefTests.cpp b/Tests/Source/LuaRefTests.cpp index 7a587590..ed1109ee 100644 --- a/Tests/Source/LuaRefTests.cpp +++ b/Tests/Source/LuaRefTests.cpp @@ -533,6 +533,71 @@ TEST_F(LuaRefTests, CallableWithHandler) EXPECT_TRUE(errorMessage.find("we failed badly") != std::string::npos); } +TEST_F(LuaRefTests, CallableWrapper) +{ + runLua("function sum(a, b) return a + b end"); + auto sumRef = luabridge::getGlobal(L, "sum"); + + auto sumFn = sumRef.callable(); + EXPECT_TRUE(sumFn.isValid()); + + { + auto result = sumFn(20, 22); + ASSERT_TRUE(result); + EXPECT_EQ(42, *result); + } + + { + auto result = sumFn.call(1, 2); + ASSERT_TRUE(result); + EXPECT_EQ(3, *result); + } + + runLua("function sumFail(a, b) error('sum failed') end"); + auto sumFailRef = luabridge::getGlobal(L, "sumFail"); + auto sumFailFn = sumFailRef.callable(); + + bool calledHandler = false; + auto handler = [&calledHandler](lua_State*) -> int + { + calledHandler = true; + return 0; + }; + + auto failed = sumFailFn.callWithHandler(handler, 1, 2); + EXPECT_FALSE(failed); + EXPECT_TRUE(calledHandler); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::LuaFunctionCallFailed), failed.error()); + + runLua("notCallable = 10"); + auto notCallableRef = luabridge::getGlobal(L, "notCallable"); + auto invalidFn = notCallableRef.callable(); + EXPECT_FALSE(invalidFn.isValid()); +} + +TEST_F(LuaRefTests, CallableWrapperVoidAndTypedMismatch) +{ + runLua("pingCalls = 0 " + "function ping() pingCalls = pingCalls + 1 end " + "function returnText() return 'abc' end"); + + auto pingRef = luabridge::getGlobal(L, "ping"); + auto pingFn = pingRef.callable(); + ASSERT_TRUE(pingFn.isValid()); + + auto pingResult = pingFn(); + ASSERT_TRUE(pingResult); + EXPECT_EQ(1, luabridge::getGlobal(L, "pingCalls").unsafe_cast()); + + auto returnTextRef = luabridge::getGlobal(L, "returnText"); + auto typedFn = returnTextRef.callable(); + ASSERT_TRUE(typedFn.isValid()); + + auto mismatch = typedFn.call(); + EXPECT_FALSE(mismatch); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTypeCast), mismatch.error()); +} + TEST_F(LuaRefTests, Pop) { lua_pushstring(L, "hello"); @@ -813,3 +878,144 @@ TEST_F(LuaRefTests, AppendToExistingSequence) ASSERT_EQ(30, result()[3].unsafe_cast()); ASSERT_EQ(40, result()[4].unsafe_cast()); } + +TEST_F(LuaRefTests, FieldHelpersRespectMetamethodsAndRawAccess) +{ + runLua("indexCalls = 0 " + "newindexCalls = 0 " + "shadow = {} " + "result = setmetatable({}, {" + " __index = function(_, key) indexCalls = indexCalls + 1; return shadow[key] end," + " __newindex = function(_, key, value) newindexCalls = newindexCalls + 1; shadow[key] = value end" + "})"); + + auto table = result(); + + EXPECT_TRUE(table.setField("metaValue", 42)); + EXPECT_EQ(1, luabridge::getGlobal(L, "newindexCalls").unsafe_cast()); + + auto viaMeta = table.getField("metaValue"); + ASSERT_TRUE(viaMeta); + EXPECT_EQ(42, *viaMeta); + EXPECT_EQ(1, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); + + auto rawMissing = table.rawgetField("metaValue"); + EXPECT_FALSE(rawMissing); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTypeCast), rawMissing.error()); + + EXPECT_TRUE(table.rawsetField("rawValue", 99)); + EXPECT_EQ(1, luabridge::getGlobal(L, "newindexCalls").unsafe_cast()); + + auto rawValue = table.rawgetField("rawValue"); + ASSERT_TRUE(rawValue); + EXPECT_EQ(99, *rawValue); +} + +TEST_F(LuaRefTests, UnsafeRawFieldHelpersKeepStackBalanced) +{ + runLua("indexCalls = 0 " + "newindexCalls = 0 " + "result = setmetatable({}, {" + " __index = function() indexCalls = indexCalls + 1; return 777 end," + " __newindex = function(_, _, _) newindexCalls = newindexCalls + 1 end" + "})"); + + auto table = result(); + const int topBeforeSet = lua_gettop(L); + + table.unsafeRawsetField("rawNumber", 55); + EXPECT_EQ(topBeforeSet, lua_gettop(L)); + EXPECT_EQ(0, luabridge::getGlobal(L, "newindexCalls").unsafe_cast()); + + const int topBeforeGet = lua_gettop(L); + auto rawNumber = table.unsafeRawgetField("rawNumber"); + EXPECT_EQ(55, rawNumber); + EXPECT_EQ(topBeforeGet, lua_gettop(L)); + EXPECT_EQ(0, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); +} + +TEST_F(LuaRefTests, RawgetFieldBypassesIndexMetamethod) +{ + runLua("indexCalls = 0 " + "result = setmetatable({}, {" + " __index = function(_, _) indexCalls = indexCalls + 1; return 123 end" + "})"); + + auto table = result(); + + auto viaMeta = table.getField("metaOnly"); + ASSERT_TRUE(viaMeta); + EXPECT_EQ(123, *viaMeta); + EXPECT_EQ(1, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); + + auto rawViaMissing = table.rawgetField("metaOnly"); + EXPECT_FALSE(rawViaMissing); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::InvalidTypeCast), rawViaMissing.error()); + EXPECT_EQ(1, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); +} + +TEST_F(LuaRefTests, UnsafeRawgetFieldBypassesIndexMetamethod) +{ + runLua("indexCalls = 0 " + "result = setmetatable({ rawValue = 999 }, {" + " __index = function(_, _) indexCalls = indexCalls + 1; return 123 end" + "})"); + + auto table = result(); + + const int topBefore = lua_gettop(L); + auto rawValue = table.unsafeRawgetField("rawValue"); + EXPECT_EQ(999, rawValue); + EXPECT_EQ(topBefore, lua_gettop(L)); + EXPECT_EQ(0, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); +} + +TEST_F(LuaRefTests, TableItemNestedRawHelpersAndCopy) +{ + runLua("result = { outer = { value = 10 } }"); + + auto outer = result()["outer"]; + auto outerCopy = outer; // Exercise TableItem copy constructor + + outerCopy.rawset(luabridge::newTable(L)); + ASSERT_TRUE(result()["outer"].isTable()); + + result()["outer"].rawset(luabridge::newTable(L)); + auto replacedOuter = result()["outer"]; + + replacedOuter.unsafeRawsetField("value", 77); + const int value = replacedOuter.unsafeRawgetField("value"); + EXPECT_EQ(77, value); + + auto rawField = replacedOuter.rawget("value"); + ASSERT_TRUE(rawField.isNumber()); + EXPECT_EQ(77, rawField.unsafe_cast()); +} + +TEST_F(LuaRefTests, TableItemOperatorIndexAdoptPathAndRawRoundTrip) +{ + runLua("result = { outer = {} }"); + + std::string outerKey = "outer"; + std::string childKey = "child"; + + auto outer = result()[outerKey]; + auto child = outer[childKey]; // Exercise TableItem::operator[](const T&) and AdoptTableRef ctor path + + child.rawset(19); + + auto fromRaw = outer.rawget(childKey); + ASSERT_TRUE(fromRaw.isNumber()); + EXPECT_EQ(19, fromRaw.unsafe_cast()); + + child.rawset(luabridge::newTable(L)); + + const int stackTopBefore = lua_gettop(L); + auto childAgain = result()[outerKey][childKey]; + childAgain.unsafeRawsetField("nested", 1234); + EXPECT_EQ(stackTopBefore, lua_gettop(L)); + + auto nested = childAgain.unsafeRawgetField("nested"); + EXPECT_EQ(1234, nested); + EXPECT_EQ(stackTopBefore, lua_gettop(L)); +} diff --git a/Tests/Source/Tests.cpp b/Tests/Source/Tests.cpp index 8d2d56b3..a9ae4d42 100644 --- a/Tests/Source/Tests.cpp +++ b/Tests/Source/Tests.cpp @@ -26,6 +26,21 @@ T identityCFunction(T value) return value; } +struct UnregisteredDecodeType +{ +}; + +int returnTupleWithUnregisteredValue(lua_State* L) +{ + lua_pushinteger(L, 17); + + lua_newuserdata(L, sizeof(UnregisteredDecodeType)); + luaL_newmetatable(L, "UnregisteredDecodeType"); + lua_setmetatable(L, -2); + + return 2; +} + int testGetter777(lua_State* L) { lua_pushinteger(L, 777); @@ -537,6 +552,22 @@ TEST_F(LuaBridgeTest, CallWithHandlerTypedReturnAndStackRestore) } } +TEST_F(LuaBridgeTest, CallTupleDecodeFailsOnUnregisteredClassResult) +{ + luabridge::setGlobal(L, static_cast(&returnTupleWithUnregisteredValue), "retTupleWithUnregistered"); + + auto f = luabridge::getGlobal(L, "retTupleWithUnregistered"); + using TupleWithUnregistered = std::tuple; + +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_ANY_THROW((void)f.call()); +#else + auto result = f.call(); + EXPECT_FALSE(result); + EXPECT_EQ(luabridge::makeErrorCode(luabridge::ErrorCode::ClassNotRegistered), result.error()); +#endif +} + TEST_F(LuaBridgeTest, IndexMetamethodSimple_ObjectFallbackBranches) { // Exercise the table-based fallback branch of index_metamethod_simple. diff --git a/Tests/Source/UserdataTests.cpp b/Tests/Source/UserdataTests.cpp index 2b201ba8..3b34b1e6 100644 --- a/Tests/Source/UserdataTests.cpp +++ b/Tests/Source/UserdataTests.cpp @@ -154,3 +154,171 @@ TEST_F(UserDataTest, FailNilRefConst) EXPECT_FALSE(runLua("testFunctionRefConst(nil)")); #endif } + +//================================================================================================= +// New Test Suite for TypeResult and getNilBadArgError Error Handling +//================================================================================================= + +namespace { +class ClassWithProperties +{ +public: + ClassWithProperties() : m_value(42), m_readOnly(100) {} + + int getValue() const { return m_value; } + void setValue(int v) { m_value = v; } + + int getReadOnly() const { return m_readOnly; } + + int callSomething(int x) { return x * 2; } + + const int& getValueRef() const { return m_value; } + +private: + int m_value; + const int m_readOnly; +}; +} // namespace + +struct TypeResultErrorHandlingTest : TestBase +{ + void SetUp() override + { + TestBase::SetUp(); + + luabridge::getGlobalNamespace(L) + .beginClass("ClassWithProperties") + .addConstructor() + .addProperty("value", &ClassWithProperties::getValue, &ClassWithProperties::setValue) + .addFunction("callSomething", &ClassWithProperties::callSomething) + .endClass() + .addFunction("getPropertyRef", [](const ClassWithProperties& obj) { return obj.getValue(); }); + } +}; + +// Test 1: Verify error message when nil is passed to reference parameters +TEST_F(TypeResultErrorHandlingTest, NilToReferenceParameterError) +{ +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("getPropertyRef(nil)"), std::runtime_error); +#else + auto [result, errorMsg] = runLuaCaptureError("getPropertyRef(nil)"); + ASSERT_FALSE(result); + // Should mention "no value" in error message + EXPECT_TRUE(errorMsg.find("no value") != std::string::npos + || errorMsg.find("ClassWithProperties") != std::string::npos); +#endif +} + +// Test 2: Verify property getter error message with nil +TEST_F(TypeResultErrorHandlingTest, PropertyGetterWithNil) +{ +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("obj = nil; v = obj.value"), std::runtime_error); +#else + auto [result, errorMsg] = runLuaCaptureError("obj = nil; v = obj.value"); + ASSERT_FALSE(result); +#endif +} + +// Test 3: Verify property setter error message with nil +TEST_F(TypeResultErrorHandlingTest, PropertySetterWithNil) +{ +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("obj = nil; obj.value = 123"), std::runtime_error); +#else + auto [result, errorMsg] = runLuaCaptureError("obj = nil; obj.value = 123"); + ASSERT_FALSE(result); +#endif +} + +// Test 4: Verify member function error with nil +TEST_F(TypeResultErrorHandlingTest, MemberFunctionWithNil) +{ +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("obj = nil; result = obj:callSomething(5)"), std::runtime_error); +#else + auto [result, errorMsg] = runLuaCaptureError("obj = nil; result = obj:callSomething(5)"); + ASSERT_FALSE(result); +#endif +} + +// Test 5: Verify correct behavior with valid object +TEST_F(TypeResultErrorHandlingTest, ValidObjectPropertyAccess) +{ + runLua("obj = ClassWithProperties(); result = obj.value"); + ASSERT_EQ(result(), 42); +} + +// Test 6: Verify correct behavior with property setter +TEST_F(TypeResultErrorHandlingTest, ValidObjectPropertySetter) +{ + runLua("obj = ClassWithProperties(); obj.value = 99; result = obj.value"); + ASSERT_EQ(result(), 99); +} + +// Test 7: Verify correct behavior with member function +TEST_F(TypeResultErrorHandlingTest, ValidObjectMemberFunction) +{ + runLua("obj = ClassWithProperties(); result = obj:callSomething(21)"); + ASSERT_EQ(result(), 42); +} + +// Test 8: Wrong type passed to member function +TEST_F(TypeResultErrorHandlingTest, WrongTypeToMemberFunction) +{ +#if LUABRIDGE_HAS_EXCEPTIONS + runLua("obj = ClassWithProperties()"); // Setup valid object + ASSERT_THROW(runLua("result = obj:callSomething('string')"), std::runtime_error); +#else + runLua("obj = ClassWithProperties()"); + auto [result, errorMsg] = runLuaCaptureError("result = obj:callSomething('string')"); + ASSERT_FALSE(result); +#endif +} + +// Test 9: Test error message quality for unregistered class +TEST_F(TypeResultErrorHandlingTest, UnregisteredClassError) +{ +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("local ud = debug.setlocal(1, nil, nil); " + "getPropertyRef(ud)"), std::runtime_error); +#else + // This is harder to test without manually constructing an unregistered userdata +#endif +} + +// Test 10: Nil handling consistency across different property access patterns +TEST_F(TypeResultErrorHandlingTest, PropertyAccessConsistency) +{ + runLua("obj = ClassWithProperties(); " + "v1 = obj.value; " + "result = (v1 == 42)"); + + ASSERT_EQ(result(), true); +} + +// Test 11: Multiple operations in sequence to verify stack management +TEST_F(TypeResultErrorHandlingTest, SequentialPropertyAccess) +{ + runLua("obj = ClassWithProperties(); " + "v1 = obj.value; " + "obj.value = 50; " + "v2 = obj.value; " + "result = v2"); + + ASSERT_EQ(result(), 50); +} + +// Test 12: Verify error when passing wrong object type to function expecting specific class +TEST_F(TypeResultErrorHandlingTest, WrongClassTypeError) +{ + runLua("obj = ClassWithProperties()"); + +#if LUABRIDGE_HAS_EXCEPTIONS + ASSERT_THROW(runLua("otherObj = TestClass(10)"), std::runtime_error); +#else + // This test mainly checks that registering TestClass succeeds + EXPECT_TRUE(true); +#endif +} From 4f6cd6989155ca7ccd3bd68eb32ad1140c547e03 Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 21:59:00 +0200 Subject: [PATCH 09/10] More coverage --- Tests/Source/LuaRefTests.cpp | 39 ++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/Tests/Source/LuaRefTests.cpp b/Tests/Source/LuaRefTests.cpp index ed1109ee..00d4913a 100644 --- a/Tests/Source/LuaRefTests.cpp +++ b/Tests/Source/LuaRefTests.cpp @@ -970,6 +970,41 @@ TEST_F(LuaRefTests, UnsafeRawgetFieldBypassesIndexMetamethod) EXPECT_EQ(0, luabridge::getGlobal(L, "indexCalls").unsafe_cast()); } +TEST_F(LuaRefTests, UserdataIndexMetamethodPropgetFastPath) +{ + struct PropsClass + { + int value = 7; + }; + + int getterCalls = 0; + + luabridge::getGlobalNamespace(L) + .beginClass("PropsClass") + .addConstructor() + .addProperty("tracked", [&getterCalls](const PropsClass* self) { + ++getterCalls; + return self->value; + }) + .endClass(); + + runLua("obj = PropsClass(); result = obj"); + auto obj = result(); + ASSERT_TRUE(obj.isUserdata()); + + auto tracked = obj["tracked"]; + auto trackedValue = luabridge::LuaRef(tracked); + ASSERT_TRUE(trackedValue.isNumber()); + EXPECT_EQ(7, luabridge::unsafe_cast(trackedValue)); + EXPECT_EQ(1, getterCalls); + + // Missing key exercises the propget fast-path miss branch (line 556 pop) and falls through to nil. + auto missing = obj["definitely_missing"]; + auto missingValue = luabridge::LuaRef(missing); + EXPECT_TRUE(missingValue.isNil()); + EXPECT_EQ(1, getterCalls); +} + TEST_F(LuaRefTests, TableItemNestedRawHelpersAndCopy) { runLua("result = { outer = { value = 10 } }"); @@ -989,7 +1024,7 @@ TEST_F(LuaRefTests, TableItemNestedRawHelpersAndCopy) auto rawField = replacedOuter.rawget("value"); ASSERT_TRUE(rawField.isNumber()); - EXPECT_EQ(77, rawField.unsafe_cast()); + EXPECT_EQ(77, luabridge::cast(rawField).valueOr(0)); } TEST_F(LuaRefTests, TableItemOperatorIndexAdoptPathAndRawRoundTrip) @@ -1006,7 +1041,7 @@ TEST_F(LuaRefTests, TableItemOperatorIndexAdoptPathAndRawRoundTrip) auto fromRaw = outer.rawget(childKey); ASSERT_TRUE(fromRaw.isNumber()); - EXPECT_EQ(19, fromRaw.unsafe_cast()); + EXPECT_EQ(19, luabridge::unsafe_cast(fromRaw)); child.rawset(luabridge::newTable(L)); From 82ca050b415e262ce03ff01464b4d152e1ade74c Mon Sep 17 00:00:00 2001 From: kunitoki Date: Sun, 29 Mar 2026 22:09:33 +0200 Subject: [PATCH 10/10] More fixes --- Source/LuaBridge/detail/CFunctions.h | 9 --------- Source/LuaBridge/detail/Namespace.h | 2 -- 2 files changed, 11 deletions(-) diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index ad1517d4..dcd98e57 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -883,10 +883,7 @@ inline int newindex_metamethod_simple(lua_State* L) const char* key = lua_tostring(L, 2); if (! lua_istable(L, lua_upvalueindex(1))) - { luaL_error(L, "no writable member '%s'", key); - return 0; - } lua_pushvalue(L, 2); // Stack: key lua_rawget(L, lua_upvalueindex(1)); // Stack: setter | nil @@ -900,7 +897,6 @@ inline int newindex_metamethod_simple(lua_State* L) } luaL_error(L, "no writable member '%s'", key); - return 0; } } else @@ -911,10 +907,7 @@ inline int newindex_metamethod_simple(lua_State* L) const char* key = lua_tostring(L, 2); if (! lua_istable(L, lua_upvalueindex(1))) - { luaL_error(L, "no writable member '%s'", key); - return 0; - } lua_pushvalue(L, 2); // Stack: key lua_rawget(L, lua_upvalueindex(1)); // Stack: setter | nil @@ -927,7 +920,6 @@ inline int newindex_metamethod_simple(lua_State* L) } luaL_error(L, "no writable member '%s'", key); - return 0; } } @@ -955,7 +947,6 @@ 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 84c54b91..7c742fdf 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -138,8 +138,6 @@ class Namespace : public detail::Registrar s = s + message; luaL_error(L, "%s", s.c_str()); - - return 0; } #endif