diff --git a/.github/workflows/amalgamate.yml b/.github/workflows/amalgamate.yml index 8b87bd95..0043c811 100644 --- a/.github/workflows/amalgamate.yml +++ b/.github/workflows/amalgamate.yml @@ -6,8 +6,8 @@ on: - master paths: - "**/workflows/amalgamate.yml" - - "**/Source/**" - - "**/amalgamate.py" + - "**/Source/**/*.h" + - "**/Tools/amalgamate.py" jobs: run: @@ -23,7 +23,7 @@ jobs: python-version: 3.9 - name: Create amalgamation file - run: python amalgamate.py + run: python Tools/amalgamate.py - name: Commit changes uses: EndBug/add-and-commit@v10 diff --git a/.github/workflows/build_asan.yml b/.github/workflows/build_asan.yml index 82af7a4e..24f7db7e 100644 --- a/.github/workflows/build_asan.yml +++ b/.github/workflows/build_asan.yml @@ -53,8 +53,10 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} @@ -63,6 +65,8 @@ jobs: ASAN_OPTIONS: detect_leaks=0:detect_odr_violation=0 run: | ./LuaBridgeTests${{ matrix.lua.suffix }} + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 06430c9c..8e96d6dd 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -53,16 +53,20 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} working-directory: ${{runner.workspace}}/build/Tests run: | ./LuaBridgeTests${{ matrix.lua.suffix }} + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept luajit: @@ -90,13 +94,17 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTestsLuaJIT \ - LuaBridgeTestsLuaJITNoexcept + LuaBridgeTestsLuaJITReflect \ + LuaBridgeTestsLuaJITNoexcept \ + LuaBridgeTestsLuaJITReflectNoexcept - name: Test LuaJIT working-directory: ${{runner.workspace}}/build/Tests run: | ./LuaBridgeTestsLuaJIT + ./LuaBridgeTestsLuaJITReflect ./LuaBridgeTestsLuaJITNoexcept + ./LuaBridgeTestsLuaJITReflectNoexcept luau: runs-on: ubuntu-latest @@ -118,11 +126,16 @@ jobs: - name: Build Luau working-directory: ${{runner.workspace}}/build - run: cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target LuaBridgeTestsLuau + run: | + cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ + LuaBridgeTestsLuau \ + LuaBridgeTestsLuauReflect - name: Test Luau working-directory: ${{runner.workspace}}/build/Tests - run: ./LuaBridgeTestsLuau + run: | + ./LuaBridgeTestsLuau + ./LuaBridgeTestsLuauReflect ravi: runs-on: ubuntu-latest @@ -144,8 +157,13 @@ jobs: - name: Build Ravi working-directory: ${{runner.workspace}}/build - run: cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target LuaBridgeTestsRavi + run: | + cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ + LuaBridgeTestsRavi \ + LuaBridgeTestsRaviReflect - name: Test Ravi working-directory: ${{runner.workspace}}/build/Tests - run: ./LuaBridgeTestsRavi + run: | + ./LuaBridgeTestsRavi + ./LuaBridgeTestsRaviReflect diff --git a/.github/workflows/build_macos.yml b/.github/workflows/build_macos.yml index 0ff61011..aab1980b 100644 --- a/.github/workflows/build_macos.yml +++ b/.github/workflows/build_macos.yml @@ -50,16 +50,20 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} working-directory: ${{runner.workspace}}/build/Tests run: | ./LuaBridgeTests${{ matrix.lua.suffix }} + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept luajit: @@ -82,13 +86,17 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTestsLuaJIT \ - LuaBridgeTestsLuaJITNoexcept + LuaBridgeTestsLuaJITReflect \ + LuaBridgeTestsLuaJITNoexcept \ + LuaBridgeTestsLuaJITReflectNoexcept - name: Test LuaJIT working-directory: ${{runner.workspace}}/build/Tests run: | ./LuaBridgeTestsLuaJIT + ./LuaBridgeTestsLuaJITReflect ./LuaBridgeTestsLuaJITNoexcept + ./LuaBridgeTestsLuaJITReflectNoexcept luau: runs-on: macos-latest @@ -107,11 +115,16 @@ jobs: - name: Build Luau working-directory: ${{runner.workspace}}/build - run: cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target LuaBridgeTestsLuau + run: | + cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ + LuaBridgeTestsLuau \ + LuaBridgeTestsLuauReflect - name: Test Luau working-directory: ${{runner.workspace}}/build/Tests - run: ./LuaBridgeTestsLuau + run: | + ./LuaBridgeTestsLuau + ./LuaBridgeTestsLuauReflect ravi: runs-on: macos-latest @@ -130,8 +143,13 @@ jobs: - name: Build Ravi working-directory: ${{runner.workspace}}/build - run: cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target LuaBridgeTestsRavi + run: | + cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ + LuaBridgeTestsRavi \ + LuaBridgeTestsRaviReflect - name: Test Ravi working-directory: ${{runner.workspace}}/build/Tests - run: ./LuaBridgeTestsRavi + run: | + ./LuaBridgeTestsRavi + ./LuaBridgeTestsRaviReflect diff --git a/.github/workflows/build_tsan.yml_ b/.github/workflows/build_tsan.yml_ index f0dbed88..549e64e2 100644 --- a/.github/workflows/build_tsan.yml_ +++ b/.github/workflows/build_tsan.yml_ @@ -53,8 +53,10 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} @@ -63,6 +65,8 @@ jobs: TSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 run: | ./LuaBridgeTests${{ matrix.lua.suffix }} + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept diff --git a/.github/workflows/build_ubsan.yml b/.github/workflows/build_ubsan.yml index 88691cf4..e826304e 100644 --- a/.github/workflows/build_ubsan.yml +++ b/.github/workflows/build_ubsan.yml @@ -53,8 +53,10 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} @@ -63,6 +65,8 @@ jobs: UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 run: | ./LuaBridgeTests${{ matrix.lua.suffix }} + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index a67bb15b..55987944 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -52,8 +52,10 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel 4 --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} @@ -61,8 +63,10 @@ jobs: shell: bash run: | ./LuaBridgeTests${{ matrix.lua.suffix }}.exe + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect.exe ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC.exe ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept.exe + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept.exe ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept.exe luajit: @@ -87,14 +91,18 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel 4 --target \ LuaBridgeTestsLuaJIT \ - LuaBridgeTestsLuaJITNoexcept + LuaBridgeTestsLuaJITReflect \ + LuaBridgeTestsLuaJITNoexcept \ + LuaBridgeTestsLuaJITReflectNoexcept - name: Test LuaJIT working-directory: ${{runner.workspace}}/build/Tests/Release shell: bash run: | ./LuaBridgeTestsLuaJIT.exe + ./LuaBridgeTestsLuaJITReflect.exe ./LuaBridgeTestsLuaJITNoexcept.exe + ./LuaBridgeTestsLuaJITReflectNoexcept.exe luau: runs-on: windows-latest @@ -115,12 +123,17 @@ jobs: - name: Build Luau working-directory: ${{runner.workspace}}/build shell: bash - run: cmake --build . --config $BUILD_TYPE --parallel 4 --target LuaBridgeTestsLuau + run: | + cmake --build . --config $BUILD_TYPE --parallel 4 --target \ + LuaBridgeTestsLuau \ + LuaBridgeTestsLuauReflect - name: Test Luau working-directory: ${{runner.workspace}}/build/Tests/Release shell: bash - run: ./LuaBridgeTestsLuau.exe + run: | + ./LuaBridgeTestsLuau.exe + ./LuaBridgeTestsLuauReflect.exe ravi: runs-on: windows-latest @@ -141,7 +154,10 @@ jobs: - name: Build Ravi working-directory: ${{runner.workspace}}/build shell: bash - run: cmake --build . --config $BUILD_TYPE --parallel 4 --target LuaBridgeTestsRavi + run: | + cmake --build . --config $BUILD_TYPE --parallel 4 --target \ + LuaBridgeTestsRavi \ + LuaBridgeTestsRaviReflect - name: Test Ravi working-directory: ${{runner.workspace}}/build/Tests/Release diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a558dfd9..44acd1a2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -55,16 +55,20 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTests${{ matrix.lua.suffix }} \ + LuaBridgeTests${{ matrix.lua.suffix }}Reflect \ LuaBridgeTests${{ matrix.lua.suffix }}LuaC \ LuaBridgeTests${{ matrix.lua.suffix }}Noexcept \ + LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept \ LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Test Lua ${{ matrix.lua.version }} working-directory: ${{runner.workspace}}/build/Tests run: | ./LuaBridgeTests${{ matrix.lua.suffix }} + ./LuaBridgeTests${{ matrix.lua.suffix }}Reflect ./LuaBridgeTests${{ matrix.lua.suffix }}LuaC ./LuaBridgeTests${{ matrix.lua.suffix }}Noexcept + ./LuaBridgeTests${{ matrix.lua.suffix }}ReflectNoexcept ./LuaBridgeTests${{ matrix.lua.suffix }}LuaCNoexcept - name: Coverage Lua ${{ matrix.lua.version }} @@ -106,13 +110,17 @@ jobs: run: | cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ LuaBridgeTestsLuaJIT \ - LuaBridgeTestsLuaJITNoexcept + LuaBridgeTestsLuaJITReflect \ + LuaBridgeTestsLuaJITNoexcept \ + LuaBridgeTestsLuaJITReflectNoexcept - name: Test LuaJIT working-directory: ${{runner.workspace}}/build/Tests run: | ./LuaBridgeTestsLuaJIT + ./LuaBridgeTestsLuaJITReflect ./LuaBridgeTestsLuaJITNoexcept + ./LuaBridgeTestsLuaJITReflectNoexcept - name: Coverage LuaJIT working-directory: ${{runner.workspace}}/build @@ -150,11 +158,16 @@ jobs: - name: Build Luau working-directory: ${{runner.workspace}}/build - run: cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target LuaBridgeTestsLuau + run: | + cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ + LuaBridgeTestsLuau \ + LuaBridgeTestsLuauReflect - name: Test Luau working-directory: ${{runner.workspace}}/build/Tests - run: ./LuaBridgeTestsLuau + run: | + ./LuaBridgeTestsLuau + ./LuaBridgeTestsLuauReflect - name: Coverage Luau working-directory: ${{runner.workspace}}/build @@ -192,11 +205,16 @@ jobs: - name: Build Ravi working-directory: ${{runner.workspace}}/build - run: cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target LuaBridgeTestsRavi + run: | + cmake --build . --config $BUILD_TYPE --parallel $(nproc) --target \ + LuaBridgeTestsRavi \ + LuaBridgeTestsRaviReflect - name: Test Ravi working-directory: ${{runner.workspace}}/build/Tests - run: LD_PRELOAD=$(gcc -print-file-name=libasan.so) ./LuaBridgeTestsRavi + run: | + LD_PRELOAD=$(gcc -print-file-name=libasan.so) ./LuaBridgeTestsRavi + LD_PRELOAD=$(gcc -print-file-name=libasan.so) ./LuaBridgeTestsRaviReflect - name: Coverage Ravi working-directory: ${{runner.workspace}}/build @@ -299,7 +317,7 @@ jobs: #- name: Convert to Coverage TXT # working-directory: ${{runner.workspace}}/build - # run: python3 ${{runner.workspace}}/cobertura.py coverage/cobertura.info coverage/coverage.txt + # run: python3 ${{runner.workspace}}/Tools/cobertura.py coverage/cobertura.info coverage/coverage.txt - name: Upload Cobertura XML uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index a79065db..dc1f1114 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set (LUABRIDGE_SANITIZE "" CACHE STRING "Sanitizer to enable (address, undefined add_subdirectory (Source) -if (LUABRIDGE_TESTING) +if (PROJECT_IS_TOP_LEVEL AND LUABRIDGE_TESTING) include (CTest) enable_testing() set (gtest_force_shared_crt ON CACHE BOOL "Use /MD and /MDd" FORCE) diff --git a/Distribution/LuaBridge/LuaBridge.h b/Distribution/LuaBridge/LuaBridge.h index 174e4e3e..bcd226c7 100644 --- a/Distribution/LuaBridge/LuaBridge.h +++ b/Distribution/LuaBridge/LuaBridge.h @@ -10,7 +10,10 @@ #include #include #include +#if defined(__has_include) && __has_include() #include +#endif + #include #include #include @@ -539,6 +542,27 @@ struct remove_first_type> template using remove_first_type_t = typename remove_first_type::type; +template +struct getter_return_type +{ + using type = void; +}; + +template +struct getter_return_type>> +{ + using type = std::remove_const_t; +}; + +template +struct getter_return_type>> +{ + using type = remove_cvref_t>; +}; + +template +using getter_return_t = typename getter_return_type::type; + } } @@ -1030,6 +1054,41 @@ void* lua_newuserdata_aligned(lua_State* L, Args&&... args) return pointer; } +inline int lua_resume_x(lua_State* L, lua_State* from, int nargs, int* nresults = nullptr) +{ +#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 + unused(from); + int status = lua_resume(L, nargs); + if (nresults) + *nresults = lua_gettop(L); + return status; +#elif LUABRIDGE_ON_LUAU || LUABRIDGE_ON_RAVI || LUA_VERSION_NUM < 504 + int status = lua_resume(L, from, nargs); + if (nresults) + *nresults = lua_gettop(L); + return status; +#else + int nr = 0; + int status = lua_resume(L, from, nargs, &nr); + if (nresults) + *nresults = nr; + return status; +#endif +} + +inline bool lua_isyieldable_x(lua_State* L) +{ +#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 || LUABRIDGE_ON_LUAU + unused(L); + return false; +#elif LUA_VERSION_NUM < 503 + unused(L); + return true; +#else + return lua_isyieldable(L) != 0; +#endif +} + [[noreturn]] inline void raise_lua_error(lua_State* L, const char* fmt, ...) { va_list argp; @@ -1159,41 +1218,6 @@ bool is_floating_point_representable_by(lua_State* L, int index) return isValid ? is_floating_point_representable_by(value) : false; } -inline int lua_resume_x(lua_State* L, lua_State* from, int nargs, int* nresults = nullptr) -{ -#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 - unused(from); - int status = lua_resume(L, nargs); - if (nresults) - *nresults = lua_gettop(L); - return status; -#elif LUABRIDGE_ON_LUAU || LUABRIDGE_ON_RAVI || LUA_VERSION_NUM < 504 - int status = lua_resume(L, from, nargs); - if (nresults) - *nresults = lua_gettop(L); - return status; -#else - int nr = 0; - int status = lua_resume(L, from, nargs, &nr); - if (nresults) - *nresults = nr; - return status; -#endif -} - -inline bool lua_isyieldable_x(lua_State* L) -{ -#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 || LUABRIDGE_ON_LUAU - unused(L); - return false; -#elif LUA_VERSION_NUM < 503 - unused(L); - return true; -#else - return lua_isyieldable(L) != 0; -#endif -} - } @@ -3260,6 +3284,11 @@ template ().find_first_of('.')> return reinterpret_cast(0xdad); } +[[nodiscard]] inline const void* getPropTypeKey() noexcept +{ + return reinterpret_cast(0x9e7); +} + [[nodiscard]] inline const void* getCastTableKey() noexcept { return reinterpret_cast(0xca57); @@ -6062,78 +6091,6 @@ inline void dumpState(lua_State* L, unsigned maxDepth = 1, std::ostream& stream // End File: Source/LuaBridge/Dump.h -// Begin File: Source/LuaBridge/List.h - -namespace luabridge { - -template -struct Stack> -{ - using Type = std::list; - - [[nodiscard]] static Result push(lua_State* L, const Type& list) - { -#if LUABRIDGE_SAFE_STACK_CHECKS - if (! lua_checkstack(L, 3)) - return makeErrorCode(ErrorCode::LuaStackOverflow); -#endif - - StackRestore stackRestore(L); - - lua_createtable(L, static_cast(list.size()), 0); - - auto it = list.cbegin(); - for (lua_Integer tableIndex = 1; it != list.cend(); ++tableIndex, ++it) - { - lua_pushinteger(L, tableIndex); - - auto result = Stack::push(L, *it); - if (! result) - return result; - - lua_settable(L, -3); - } - - stackRestore.reset(); - return {}; - } - - [[nodiscard]] static TypeResult get(lua_State* L, int index) - { - if (!lua_istable(L, index)) - return makeErrorCode(ErrorCode::InvalidTypeCast); - - const StackRestore stackRestore(L); - - Type list; - - int absIndex = lua_absindex(L, index); - lua_pushnil(L); - - while (lua_next(L, absIndex) != 0) - { - auto item = Stack::get(L, -1); - if (! item) - return makeErrorCode(ErrorCode::InvalidTypeCast); - - list.emplace_back(*item); - lua_pop(L, 1); - } - - return list; - } - - [[nodiscard]] static bool isInstance(lua_State* L, int index) - { - return lua_istable(L, index); - } -}; - -} - - -// End File: Source/LuaBridge/List.h - // Begin File: Source/LuaBridge/detail/FlagSet.h namespace luabridge { @@ -7541,6 +7498,31 @@ inline void add_property_getter(lua_State* L, const char* name, int tableIndex) lua_pop(L, 2); } +inline void add_property_type(lua_State* L, const char* name, const char* typeName, int tableIndex) +{ +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(L, 3, detail::error_lua_stack_overflow); +#endif + + if (!typeName || typeName[0] == '\0') + return; + + tableIndex = lua_absindex(L, tableIndex); + + lua_rawgetp_x(L, tableIndex, getPropTypeKey()); + if (lua_isnil(L, -1)) + { + lua_pop(L, 1); + lua_createtable(L, 0, 4); + lua_pushvalue(L, -1); + lua_rawsetp_x(L, tableIndex, getPropTypeKey()); + } + + lua_pushstring(L, typeName); + rawsetfield(L, -2, name); + lua_pop(L, 1); +} + template struct property_setter; @@ -7807,35 +7789,35 @@ struct function } }; -template +template int invoke_member_function(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); 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))); + const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(UV))); LUABRIDGE_ASSERT(func != nullptr); return function::call(L, *ptr, func); } -template +template int invoke_const_member_function(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); 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))); + const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(UV))); LUABRIDGE_ASSERT(func != nullptr); return function::call(L, *ptr, func); @@ -7903,27 +7885,27 @@ int invoke_const_member_cfunction(lua_State* L) #endif } -template +template int invoke_proxy_function(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(lua_islightuserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(lua_islightuserdata(L, lua_upvalueindex(UV))); - auto func = reinterpret_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto func = reinterpret_cast(lua_touserdata(L, lua_upvalueindex(UV))); LUABRIDGE_ASSERT(func != nullptr); return function::call(L, func); } -template +template int invoke_proxy_functor(lua_State* L) { using FnTraits = function_traits>; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); - auto& func = *align>(lua_touserdata(L, lua_upvalueindex(1))); + auto& func = *align>(lua_touserdata(L, lua_upvalueindex(UV))); return function::call(L, func); } @@ -7948,14 +7930,14 @@ inline int invoke_safe_cfunction(lua_State* L) } #endif -template +template int invoke_proxy_constructor(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); - auto& func = *align(lua_touserdata(L, lua_upvalueindex(1))); + auto& func = *align(lua_touserdata(L, lua_upvalueindex(UV))); function::call(L, func); @@ -7982,10 +7964,18 @@ struct OverloadEntry int arity; TypeChecker checker; + +#if LUABRIDGE_ENABLE_REFLECT + std::string returnType; + std::vector paramTypes; + std::vector paramHints; +#endif }; struct OverloadSet { + static constexpr uint32_t kMagic = 0x4C425246u; + uint32_t magic = kMagic; std::vector entries; }; @@ -8131,6 +8121,29 @@ inline void push_function(lua_State* L, F&& f, const char* debugname) lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 1); } +template +inline void push_function_reflect(lua_State* L, ReturnType (*fp)(Params...), const char* debugname) +{ + using FnType = ReturnType (*)(Params...); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +inline void push_function_reflect(lua_State* L, ReturnType (*fp)(Params...) noexcept, const char* debugname) +{ + using FnType = ReturnType (*)(Params...) noexcept; + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template && !std::is_pointer_v && !std::is_member_function_pointer_v>> +inline void push_function_reflect(lua_State* L, F&& f, const char* debugname) +{ + lua_newuserdata_aligned(L, std::forward(f)); + lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 2); +} + template void push_member_function(lua_State* L, lua_CFunction fp, const char* debugname) { @@ -8293,58 +8306,170 @@ void push_member_function(lua_State* L, int (U::*mfp)(lua_State*) const, const c lua_pushcclosure_x(L, &invoke_const_member_cfunction, debugname, 1); } -template || !detail::is_callable_v)>> -void push_property_getter(lua_State* L, const T* value, const char* debugname) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T*, Params...), const char* debugname) { - lua_pushlightuserdata(L, reinterpret_cast(const_cast(value))); - lua_pushcclosure_x(L, &property_getter::call, debugname, 1); + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); } -template -void push_property_getter(lua_State* L, T (*getter)(), const char* debugname) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T&, Params...), const char* debugname) { - lua_pushlightuserdata(L, reinterpret_cast(getter)); - lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 1); + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); } -template -void push_property_getter(lua_State* L, T (*getter)() noexcept, const char* debugname) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T*, Params...) noexcept, const char* debugname) { - lua_pushlightuserdata(L, reinterpret_cast(getter)); - lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 1); + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); } -template >> -void push_property_getter(lua_State* L, T getter, const char* debugname) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T&, Params...) noexcept, const char* debugname) { - if constexpr (std::is_same_v) - { - lua_pushcfunction_x(L, getter, debugname); - } - else - { - lua_newuserdata_aligned(L, std::move(getter)); - lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 1); - } + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); } -template -void push_class_property_getter(lua_State* L, T (U::*value), const char* debugname) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T*, Params...), const char* debugname) { - using MemberValue = decltype(value); - - new (lua_newuserdata_x(L, sizeof(MemberValue))) MemberValue(value); - lua_pushcclosure_x(L, &property_getter::call, debugname, 1); + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); } -template -void push_class_property_getter(lua_State* L, T (C::*getter)() const, const char* debugname) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T&, Params...), const char* debugname) { - using GetType = decltype(getter); + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} - new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T*, Params...) noexcept, const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T&, Params...) noexcept, const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...), const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...) noexcept, const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...) const, const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...) const noexcept, const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 2); +} + +template && + std::is_object_v && + !std::is_pointer_v && + !std::is_member_function_pointer_v>> +void push_member_function_reflect(lua_State* L, F&& f, const char* debugname) +{ + static_assert(std::is_same_v>>>); + lua_newuserdata_aligned(L, std::forward(f)); + lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 2); +} + +template || !detail::is_callable_v)>> +void push_property_getter(lua_State* L, const T* value, const char* debugname) +{ + lua_pushlightuserdata(L, reinterpret_cast(const_cast(value))); + lua_pushcclosure_x(L, &property_getter::call, debugname, 1); +} + +template +void push_property_getter(lua_State* L, T (*getter)(), const char* debugname) +{ + lua_pushlightuserdata(L, reinterpret_cast(getter)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 1); +} + +template +void push_property_getter(lua_State* L, T (*getter)() noexcept, const char* debugname) +{ + lua_pushlightuserdata(L, reinterpret_cast(getter)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 1); +} + +template >> +void push_property_getter(lua_State* L, T getter, const char* debugname) +{ + if constexpr (std::is_same_v) + { + lua_pushcfunction_x(L, getter, debugname); + } + else + { + lua_newuserdata_aligned(L, std::move(getter)); + lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 1); + } +} + +template +void push_class_property_getter(lua_State* L, T (U::*value), const char* debugname) +{ + using MemberValue = decltype(value); + + new (lua_newuserdata_x(L, sizeof(MemberValue))) MemberValue(value); + lua_pushcclosure_x(L, &property_getter::call, debugname, 1); +} + +template +void push_class_property_getter(lua_State* L, T (C::*getter)() const, const char* debugname) +{ + using GetType = decltype(getter); + + new (lua_newuserdata_x(L, sizeof(GetType))) GetType(getter); lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 1); } @@ -8614,16 +8739,30 @@ struct constructor { static T* construct(const Args& args) { - auto alloc = [](auto&&... args) { return new T(std::forward(args)...); }; - - return std::apply(alloc, args); + if constexpr (std::is_aggregate_v) + { + auto alloc = [](auto&&... args) { return new T{std::forward(args)...}; }; + return std::apply(alloc, args); + } + else + { + auto alloc = [](auto&&... args) { return new T(std::forward(args)...); }; + return std::apply(alloc, args); + } } static T* construct(void* ptr, const Args& args) { - auto alloc = [ptr](auto&&... args) { return new (ptr) T(std::forward(args)...); }; - - return std::apply(alloc, args); + if constexpr (std::is_aggregate_v) + { + auto alloc = [ptr](auto&&... args) { return new (ptr) T{std::forward(args)...}; }; + return std::apply(alloc, args); + } + else + { + auto alloc = [ptr](auto&&... args) { return new (ptr) T(std::forward(args)...); }; + return std::apply(alloc, args); + } } }; @@ -8817,64 +8956,1254 @@ struct factory_forwarder } #endif - std::error_code ec; - auto* value = UserdataValueExternal::place(L, object, m_dealloc, ec); - if (! value) - raise_lua_error(L, "%s", detail::ErrorCategory::errorString(ec.value())); + std::error_code ec; + auto* value = UserdataValueExternal::place(L, object, m_dealloc, ec); + if (! value) + raise_lua_error(L, "%s", detail::ErrorCategory::errorString(ec.value())); + + return object; + } + +private: + Alloc m_alloc; + Dealloc m_dealloc; +}; + +template +struct container_forwarder +{ + explicit container_forwarder(F f) + : m_func(std::move(f)) + { + } + + C operator()(lua_State* L) + { + using FnTraits = function_traits; + using FnArgs = typename FnTraits::argument_types; + + C object; + +#if LUABRIDGE_HAS_EXCEPTIONS + try + { +#endif + object = container_constructor::construct(m_func, make_arguments_list(L)); + +#if LUABRIDGE_HAS_EXCEPTIONS + } + catch (const std::exception& e) + { + raise_lua_error(L, "%s", e.what()); + } +#endif + + auto result = UserdataSharedHelper::push(L, object); + if (! result) + raise_lua_error(L, "%s", result.error_cstr()); + + return object; + } + +private: + F m_func; +}; + +} +} + + +// End File: Source/LuaBridge/detail/CFunctions.h + +// Begin File: Source/LuaBridge/Inspect.h + +namespace luabridge { + +struct NamespaceInspectInfo; +struct ClassInspectInfo; +class InspectVisitor; + +void accept(const NamespaceInspectInfo& ns, InspectVisitor& v); +void accept(const ClassInspectInfo& cls, InspectVisitor& v); + +enum class MemberKind +{ + Method, + StaticMethod, + Property, + ReadOnlyProperty, + StaticProperty, + StaticReadOnlyProperty, + Constructor, + Metamethod, +}; + +struct ParamInfo +{ + std::string typeName; + std::string hint; +}; + +struct OverloadInfo +{ + std::string returnType; + std::vector params; + bool isConst = false; +}; + +struct MemberInfo +{ + std::string name; + MemberKind kind = MemberKind::Method; + std::vector overloads; +}; + +struct ClassInspectInfo +{ + std::string name; + std::vector baseClasses; + std::vector members; + + void accept(InspectVisitor& v) const; +}; + +struct NamespaceInspectInfo +{ + std::string name; + std::vector freeMembers; + std::vector classes; + std::vector subNamespaces; + + void accept(InspectVisitor& v) const; +}; + +class InspectVisitor +{ +public: + virtual ~InspectVisitor() = default; + + virtual void beginNamespace(const NamespaceInspectInfo& +) {} + virtual void endNamespace(const NamespaceInspectInfo& +) {} + virtual void visitFreeMember(const NamespaceInspectInfo& +, const MemberInfo& +) {} + + virtual void beginClass(const ClassInspectInfo& +) {} + virtual void endClass(const ClassInspectInfo& +) {} + virtual void visitMember(const ClassInspectInfo& +, const MemberInfo& +) {} +}; + +inline void accept(const NamespaceInspectInfo& ns, InspectVisitor& v) +{ + v.beginNamespace(ns); + for (const auto& m : ns.freeMembers) + v.visitFreeMember(ns, m); + for (const auto& cls : ns.classes) + accept(cls, v); + for (const auto& sub : ns.subNamespaces) + accept(sub, v); + v.endNamespace(ns); +} + +inline void accept(const ClassInspectInfo& cls, InspectVisitor& v) +{ + v.beginClass(cls); + for (const auto& m : cls.members) + v.visitMember(cls, m); + v.endClass(cls); +} + +inline void ClassInspectInfo::accept(InspectVisitor& v) const { luabridge::accept(*this, v); } +inline void NamespaceInspectInfo::accept(InspectVisitor& v) const { luabridge::accept(*this, v); } + +namespace detail { + +inline std::set collectTableKeys(lua_State* L, int tableIdx) +{ + std::set keys; + tableIdx = lua_absindex(L, tableIdx); + lua_pushnil(L); + while (lua_next(L, tableIdx)) + { + if (lua_type(L, -2) == LUA_TSTRING) + keys.insert(lua_tostring(L, -2)); + lua_pop(L, 1); + } + return keys; +} + +inline std::string stripConst(const char* name) +{ + std::string s = name ? name : ""; + if (s.substr(0, 6) == "const ") + s = s.substr(6); + return s; +} + +inline std::vector getOverloadInfos(lua_State* L, int funcIdx) +{ + funcIdx = lua_absindex(L, funcIdx); + + if (!lua_isfunction(L, funcIdx)) + return { OverloadInfo{} }; + + const char* uv1name = lua_getupvalue(L, funcIdx, 1); + if (uv1name == nullptr) + { + + return { OverloadInfo{} }; + } + + OverloadSet* overload_set = nullptr; + if (isfulluserdata(L, -1)) + { + auto* candidate = align(lua_touserdata(L, -1)); + if (candidate && candidate->magic == OverloadSet::kMagic) + overload_set = candidate; + } + lua_pop(L, 1); + + if (!overload_set || overload_set->entries.empty()) + return { OverloadInfo{} }; + + std::vector result; + result.reserve(overload_set->entries.size()); + + for (const auto& entry : overload_set->entries) + { + OverloadInfo info; + +#if LUABRIDGE_ENABLE_REFLECT + info.returnType = entry.returnType; + for (std::size_t i = 0; i < entry.paramTypes.size(); ++i) + { + ParamInfo p; + p.typeName = entry.paramTypes[i]; + if (i < entry.paramHints.size()) + p.hint = entry.paramHints[i]; + info.params.push_back(std::move(p)); + } +#else + + if (entry.arity >= 0) + { + for (int i = 0; i < entry.arity; ++i) + info.params.push_back(ParamInfo{}); + } +#endif + + result.push_back(std::move(info)); + } + + return result; +} + +inline bool isClassStaticTable(lua_State* L, int tableIdx) +{ + tableIdx = lua_absindex(L, tableIdx); + + if (!lua_getmetatable(L, tableIdx)) + return false; + + lua_rawgetp_x(L, -1, getClassKey()); + bool result = lua_istable(L, -1); + lua_pop(L, 1); + + if (!result) + { + lua_pop(L, 1); + return false; + } + + return true; +} + +inline ClassInspectInfo inspectClassFromStaticTable(lua_State* L, int stIdx) +{ + stIdx = lua_absindex(L, stIdx); + + int mtIdx = lua_absindex(L, -1); + + lua_rawgetp_x(L, mtIdx, getClassKey()); + int clIdx = lua_absindex(L, -1); + + ClassInspectInfo cls; + + lua_rawgetp_x(L, clIdx, getTypeKey()); + if (lua_isstring(L, -1)) + cls.name = stripConst(lua_tostring(L, -1)); + lua_pop(L, 1); + + lua_rawgetp_x(L, clIdx, getParentKey()); + if (lua_istable(L, -1)) + { + int parIdx = lua_absindex(L, -1); + int len = get_length(L, parIdx); + for (int i = 1; i <= len; ++i) + { + lua_rawgeti(L, parIdx, i); + lua_rawgetp_x(L, -1, getTypeKey()); + if (lua_isstring(L, -1)) + { + std::string baseName = stripConst(lua_tostring(L, -1)); + if (!baseName.empty() && baseName != cls.name) + cls.baseClasses.push_back(std::move(baseName)); + } + lua_pop(L, 2); + } + } + lua_pop(L, 1); + + std::set instPropget; + lua_rawgetp_x(L, clIdx, getPropgetKey()); + if (lua_istable(L, -1)) + instPropget = collectTableKeys(L, -1); + lua_pop(L, 1); + + std::set instPropsetReal; + lua_rawgetp_x(L, clIdx, getPropsetKey()); + if (lua_istable(L, -1)) + { + int psIdx = lua_absindex(L, -1); + for (const auto& k : instPropget) + { + lua_getfield(L, psIdx, k.c_str()); + if (lua_isfunction(L, -1)) + { + lua_CFunction fn = lua_tocfunction(L, -1); + if (fn != &detail::read_only_error) + instPropsetReal.insert(k); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + std::map instPropTypes; + lua_rawgetp_x(L, clIdx, getPropTypeKey()); + if (lua_istable(L, -1)) + { + int ptIdx = lua_absindex(L, -1); + for (const auto& k : instPropget) + { + lua_getfield(L, ptIdx, k.c_str()); + if (lua_isstring(L, -1)) + instPropTypes[k] = lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + for (const auto& propName : instPropget) + { + MemberInfo m; + m.name = propName; + m.kind = instPropsetReal.count(propName) ? MemberKind::Property : MemberKind::ReadOnlyProperty; + OverloadInfo ov; + auto it = instPropTypes.find(propName); + if (it != instPropTypes.end()) + ov.returnType = it->second; + m.overloads.push_back(std::move(ov)); + cls.members.push_back(std::move(m)); + } + + static const std::set skipKeys{ "__index", "__newindex", "__metatable" }; + + lua_pushnil(L); + while (lua_next(L, clIdx)) + { + if (lua_type(L, -2) == LUA_TSTRING) + { + std::string key = lua_tostring(L, -2); + if (!skipKeys.count(key) && !instPropget.count(key)) + { + if (lua_isfunction(L, -1) || lua_isuserdata(L, -1)) + { + MemberInfo m; + m.name = key; + m.kind = (key.size() >= 2 && key[0] == '_' && key[1] == '_') + ? MemberKind::Metamethod + : MemberKind::Method; + m.overloads = getOverloadInfos(L, -1); + cls.members.push_back(std::move(m)); + } + } + } + lua_pop(L, 1); + } + + std::set stPropget; + lua_rawgetp_x(L, mtIdx, getPropgetKey()); + if (lua_istable(L, -1)) + stPropget = collectTableKeys(L, -1); + lua_pop(L, 1); + + std::set stPropsetReal; + lua_rawgetp_x(L, mtIdx, getPropsetKey()); + if (lua_istable(L, -1)) + { + int psIdx = lua_absindex(L, -1); + for (const auto& k : stPropget) + { + lua_getfield(L, psIdx, k.c_str()); + if (lua_isfunction(L, -1)) + { + lua_CFunction fn = lua_tocfunction(L, -1); + if (fn != &detail::read_only_error) + stPropsetReal.insert(k); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + std::map stPropTypes; + lua_rawgetp_x(L, mtIdx, getPropTypeKey()); + if (lua_istable(L, -1)) + { + int ptIdx = lua_absindex(L, -1); + for (const auto& k : stPropget) + { + lua_getfield(L, ptIdx, k.c_str()); + if (lua_isstring(L, -1)) + stPropTypes[k] = lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + for (const auto& propName : stPropget) + { + MemberInfo m; + m.name = propName; + m.kind = stPropsetReal.count(propName) ? MemberKind::StaticProperty : MemberKind::StaticReadOnlyProperty; + OverloadInfo ov; + auto it = stPropTypes.find(propName); + if (it != stPropTypes.end()) + ov.returnType = it->second; + m.overloads.push_back(std::move(ov)); + cls.members.push_back(std::move(m)); + } + + static const std::set staticSkipKeys{ "__index", "__newindex", "__metatable" }; + + lua_rawgetp_x(L, mtIdx, getClassKey()); + lua_pop(L, 1); + + lua_pushnil(L); + while (lua_next(L, mtIdx)) + { + if (lua_type(L, -2) == LUA_TSTRING) + { + std::string key = lua_tostring(L, -2); + if (!staticSkipKeys.count(key) && !stPropget.count(key)) + { + if (lua_isfunction(L, -1) || lua_isuserdata(L, -1)) + { + MemberInfo m; + m.name = key; + if (key == "__call") + m.kind = MemberKind::Constructor; + else if (key.size() >= 2 && key[0] == '_' && key[1] == '_') + m.kind = MemberKind::Metamethod; + else + m.kind = MemberKind::StaticMethod; + m.overloads = getOverloadInfos(L, -1); + cls.members.push_back(std::move(m)); + } + } + } + lua_pop(L, 1); + } + + lua_pop(L, 2); + return cls; +} + +inline NamespaceInspectInfo inspectNamespaceTable(lua_State* L, int nsIdx, std::string name); + +inline NamespaceInspectInfo inspectNamespaceTable(lua_State* L, int nsIdx, std::string name) +{ + nsIdx = lua_absindex(L, nsIdx); + NamespaceInspectInfo info; + info.name = std::move(name); + + std::set nsPropget; + lua_rawgetp_x(L, nsIdx, getPropgetKey()); + if (lua_istable(L, -1)) + nsPropget = collectTableKeys(L, -1); + lua_pop(L, 1); + + int nsPropsetTableIdx = 0; + lua_rawgetp_x(L, nsIdx, getPropsetKey()); + if (lua_istable(L, -1)) + nsPropsetTableIdx = lua_absindex(L, -1); + + std::map nsPropTypes; + lua_rawgetp_x(L, nsIdx, getPropTypeKey()); + if (lua_istable(L, -1)) + { + int ptIdx = lua_absindex(L, -1); + for (const auto& k : nsPropget) + { + lua_getfield(L, ptIdx, k.c_str()); + if (lua_isstring(L, -1)) + nsPropTypes[k] = lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + for (const auto& propName : nsPropget) + { + MemberInfo m; + m.name = propName; + + bool isReadOnly = true; + if (nsPropsetTableIdx) + { + lua_getfield(L, nsPropsetTableIdx, propName.c_str()); + if (lua_isfunction(L, -1)) + { + lua_CFunction fn = lua_tocfunction(L, -1); + isReadOnly = (fn == &detail::read_only_error); + } + lua_pop(L, 1); + } + + m.kind = isReadOnly ? MemberKind::ReadOnlyProperty : MemberKind::Property; + OverloadInfo ov; + auto it = nsPropTypes.find(propName); + if (it != nsPropTypes.end()) + ov.returnType = it->second; + m.overloads.push_back(std::move(ov)); + info.freeMembers.push_back(std::move(m)); + } + + lua_pop(L, 1); + + static const std::set nsSkipKeys{ "__index", "__newindex", "__metatable", "__gc" }; + + lua_pushnil(L); + while (lua_next(L, nsIdx)) + { + if (lua_type(L, -2) != LUA_TSTRING) + { + lua_pop(L, 1); + continue; + } + + std::string key = lua_tostring(L, -2); + + if (nsSkipKeys.count(key) || nsPropget.count(key)) + { + lua_pop(L, 1); + continue; + } + + if (lua_istable(L, -1)) + { + int valIdx = lua_absindex(L, -1); + + if (isClassStaticTable(L, valIdx)) + { + + auto cls = inspectClassFromStaticTable(L, valIdx); + + info.classes.push_back(std::move(cls)); + } + else + { + auto sub = inspectNamespaceTable(L, valIdx, key); + info.subNamespaces.push_back(std::move(sub)); + } + } + else if (lua_isfunction(L, -1)) + { + if (!nsPropget.count(key)) + { + MemberInfo m; + m.name = key; + m.kind = MemberKind::Method; + m.overloads = getOverloadInfos(L, -1); + info.freeMembers.push_back(std::move(m)); + } + } + + lua_pop(L, 1); + } + + return info; +} + +} + +template +[[nodiscard]] ClassInspectInfo inspect(lua_State* L) +{ +#if LUABRIDGE_ENABLE_REFLECT + + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + return {}; + } + + lua_rawgetp_x(L, -1, detail::getStaticKey()); + if (!lua_istable(L, -1)) + { + lua_pop(L, 2); + return {}; + } + + lua_rawgetp_x(L, -1, detail::getClassKey()); + const bool isClass = lua_istable(L, -1); + lua_pop(L, 1); + + if (!isClass) + { + lua_pop(L, 2); + return {}; + } + + ClassInspectInfo result = detail::inspectClassFromStaticTable(L, lua_gettop(L)); + + lua_pop(L, 1); + return result; +#else + unused(L); + return {}; +#endif +} + +[[nodiscard]] inline NamespaceInspectInfo inspectNamespace(lua_State* L, const char* namespaceName = nullptr) +{ +#if LUABRIDGE_ENABLE_REFLECT + if (namespaceName == nullptr || namespaceName[0] == '\0') + { + lua_getglobal(L, "_G"); + } + else + { + + std::string path = namespaceName; + std::string first; + std::string::size_type dot = path.find('.'); + if (dot == std::string::npos) + first = path; + else + first = path.substr(0, dot); + + lua_getglobal(L, first.c_str()); + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + return {}; + } + + while (dot != std::string::npos) + { + path = path.substr(dot + 1); + dot = path.find('.'); + std::string component = (dot == std::string::npos) ? path : path.substr(0, dot); + lua_getfield(L, -1, component.c_str()); + lua_remove(L, -2); + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + return {}; + } + } + } + + std::string label; + if (namespaceName && namespaceName[0]) + { + std::string_view sv(namespaceName); + auto dot = sv.rfind('.'); + label = std::string(dot == std::string_view::npos ? sv : sv.substr(dot + 1)); + } + else + { + label = "_G"; + } + auto result = detail::inspectNamespaceTable(L, -1, label); + lua_pop(L, 1); + return result; +#else + unused(L, namespaceName); + return {}; +#endif +} + +inline void inspectAccept(lua_State* L, const char* namespaceName, InspectVisitor& visitor) +{ +#if LUABRIDGE_ENABLE_REFLECT + auto ns = inspectNamespace(L, namespaceName); + accept(ns, visitor); +#else + unused(L, namespaceName, visitor); +#endif +} + +class ConsoleVisitor : public InspectVisitor +{ +public: + explicit ConsoleVisitor(std::ostream& out = std::cerr) + : out_(out) + { + } + + void beginNamespace(const NamespaceInspectInfo& ns) override + { + indent(); + out_ << "namespace " << ns.name << " {\n"; + ++depth_; + } + + void endNamespace([[maybe_unused]] const NamespaceInspectInfo& ns) override + { + --depth_; + indent(); + out_ << "}\n"; + } + + void visitFreeMember([[maybe_unused]] const NamespaceInspectInfo& ns, const MemberInfo& m) override + { + emitMember(m, ""); + } + + void beginClass(const ClassInspectInfo& cls) override + { + indent(); + out_ << "class " << cls.name; + if (!cls.baseClasses.empty()) + { + out_ << " extends "; + for (std::size_t i = 0; i < cls.baseClasses.size(); ++i) + { + if (i) out_ << ", "; + out_ << cls.baseClasses[i]; + } + } + out_ << " {\n"; + ++depth_; + } + + void endClass([[maybe_unused]] const ClassInspectInfo& cls) override + { + --depth_; + indent(); + out_ << "}\n"; + } + + void visitMember(const ClassInspectInfo& cls, const MemberInfo& m) override + { + emitMember(m, cls.name); + } + +private: + void indent() const + { + for (int i = 0; i < depth_; ++i) + out_ << " "; + } + + static std::string paramStr(const OverloadInfo& ov) + { + std::string s; + for (std::size_t i = 0; i < ov.params.size(); ++i) + { + if (i) s += ", "; + const auto& p = ov.params[i]; + s += p.hint.empty() ? ("p" + std::to_string(i + 1)) : p.hint; + s += ": "; + s += p.typeName.empty() ? "any" : p.typeName; + } + return s; + } + + static std::string retStr(const OverloadInfo& ov, const std::string& constructorClass = "") + { + if (!constructorClass.empty()) + return constructorClass; + return ov.returnType.empty() ? "any" : ov.returnType; + } + + void emitMember(const MemberInfo& m, [[maybe_unused]] const std::string& className) const + { + switch (m.kind) + { + case MemberKind::Property: + indent(); + out_ << m.name << ": any;\n"; + break; + + case MemberKind::ReadOnlyProperty: + indent(); + out_ << "readonly " << m.name << ": any;\n"; + break; + + case MemberKind::StaticProperty: + indent(); + out_ << "static " << m.name << ": any;\n"; + break; + + case MemberKind::StaticReadOnlyProperty: + indent(); + out_ << "static readonly " << m.name << ": any;\n"; + break; + + case MemberKind::Metamethod: + + break; + + case MemberKind::Constructor: + for (const auto& ov : m.overloads) + { + indent(); + out_ << "constructor(" << paramStr(ov) << ");\n"; + } + break; + + case MemberKind::Method: + case MemberKind::StaticMethod: + { + const bool isStatic = (m.kind == MemberKind::StaticMethod); + for (std::size_t i = 0; i < m.overloads.size(); ++i) + { + indent(); + if (isStatic) out_ << "static "; + out_ << m.name << "(" << paramStr(m.overloads[i]) << "): " << retStr(m.overloads[i]) << ";\n"; + } + break; + } + + default: + break; + } + } + + std::ostream& out_; + int depth_ = 0; +}; + +class LuaLSVisitor : public InspectVisitor +{ +public: + explicit LuaLSVisitor(std::ostream& out) + : out_(out) + { + } + + void beginNamespace(const NamespaceInspectInfo& ns) override + { + ns_ = ns.name == "_G" ? "" : ns.name; + } + + void endNamespace([[maybe_unused]] const NamespaceInspectInfo& ns) override {} + + void visitFreeMember(const NamespaceInspectInfo& ns, const MemberInfo& m) override + { + if (m.kind != MemberKind::Method && m.kind != MemberKind::StaticMethod) + return; + + for (const auto& ov : m.overloads) + { + emitParamsTo(out_, ov.params); + if (!ov.returnType.empty() && ov.returnType != "void") + out_ << "---@return " << luaType(ov.returnType) << "\n"; + } + + std::string qual = ns_.empty() ? "" : (ns_ + "."); + out_ << "function " << qual << m.name << "("; + if (!m.overloads.empty()) + out_ << paramNames(m.overloads[0].params); + out_ << ") end\n\n"; + } + + void beginClass(const ClassInspectInfo& cls) override + { + curClass_ = cls.name; + out_ << "---@class " << qualifiedName(cls.name); + if (!cls.baseClasses.empty()) + { + out_ << " : " << cls.baseClasses[0]; + for (std::size_t i = 1; i < cls.baseClasses.size(); ++i) + out_ << ", " << cls.baseClasses[i]; + } + out_ << "\n"; + methodBuf_.str({}); + methodBuf_.clear(); + } + + void endClass([[maybe_unused]] const ClassInspectInfo& cls) override + { + out_ << "local " << curClass_ << " = {}\n"; + const std::string methods = methodBuf_.str(); + if (!methods.empty()) + out_ << "\n" << methods; + else + out_ << "\n"; + curClass_.clear(); + } + + void visitMember(const ClassInspectInfo& cls, const MemberInfo& m) override + { + auto propType = [&]() -> std::string { + if (!m.overloads.empty() && !m.overloads[0].returnType.empty()) + return luaType(m.overloads[0].returnType); + return "any"; + }; + + switch (m.kind) + { + case MemberKind::Property: + out_ << "---@field " << m.name << " " << propType() << "\n"; + break; + + case MemberKind::ReadOnlyProperty: + out_ << "---@field " << m.name << " " << propType() << " # readonly\n"; + break; + + case MemberKind::StaticProperty: + out_ << "---@field " << m.name << " " << propType() << "\n"; + break; + + case MemberKind::StaticReadOnlyProperty: + out_ << "---@field " << m.name << " " << propType() << " # readonly\n"; + break; + + case MemberKind::Constructor: + { + + std::string qname = qualifiedName(cls.name); + for (const auto& ov : m.overloads) + out_ << "---@overload fun(" << overloadParams(ov.params) << "): " << qname << "\n"; + break; + } + + case MemberKind::Method: + { + std::string qname = qualifiedName(cls.name); + for (std::size_t i = 0; i < m.overloads.size(); ++i) + { + const auto& ov = m.overloads[i]; + if (i == 0) + { + emitParamsTo(methodBuf_, ov.params, +true, qname); + if (!ov.returnType.empty() && ov.returnType != "void") + methodBuf_ << "---@return " << luaType(ov.returnType) << "\n"; + methodBuf_ << "function " << qname << ":" << m.name << "(" << paramNames(ov.params) << ") end\n\n"; + } + else + { + methodBuf_ << "---@overload fun(self: " << qname << ", " << overloadParams(ov.params) << ")" + << (ov.returnType.empty() ? "" : (": " + luaType(ov.returnType))) << "\n"; + } + } + break; + } + + case MemberKind::StaticMethod: + { + std::string qname = qualifiedName(cls.name); + for (std::size_t i = 0; i < m.overloads.size(); ++i) + { + const auto& ov = m.overloads[i]; + if (i == 0) + { + emitParamsTo(methodBuf_, ov.params); + if (!ov.returnType.empty() && ov.returnType != "void") + methodBuf_ << "---@return " << luaType(ov.returnType) << "\n"; + methodBuf_ << "function " << qname << "." << m.name << "(" << paramNames(ov.params) << ") end\n\n"; + } + else + { + methodBuf_ << "---@overload fun(" << overloadParams(ov.params) << ")" + << (ov.returnType.empty() ? "" : (": " + luaType(ov.returnType))) << "\n"; + } + } + break; + } + + default: + break; + } + } + +private: + + static std::string luaType(const std::string& cppType) + { + if (cppType == "void") return "nil"; + + if (cppType == "bool") return "boolean"; + + if (cppType == "int" || cppType == "long" || cppType == "short" || + cppType == "unsigned int" || cppType == "unsigned long" || + cppType == "unsigned short" || cppType == "int64_t" || cppType == "uint64_t" || + cppType == "int32_t" || cppType == "uint32_t" || cppType == "size_t") + return "integer"; + + if (cppType == "float" || cppType == "double") return "number"; + + if (cppType == "std::string" || cppType == "const char *" || cppType == "const char*") + return "string"; + + return cppType.empty() ? "any" : cppType; + } + + std::string qualifiedName(const std::string& name) const + { + return ns_.empty() ? name : (ns_ + "." + name); + } + + static std::string paramNames(const std::vector& params) + { + std::string s; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i) s += ", "; + s += params[i].hint.empty() ? ("p" + std::to_string(i + 1)) : params[i].hint; + } + return s; + } + + static std::string overloadParams(const std::vector& params) + { + std::string s; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i) s += ", "; + + const auto& p = params[i]; + std::string pname = p.hint.empty() ? ("p" + std::to_string(i + 1)) : p.hint; + s += pname + ": " + luaType(p.typeName.empty() ? "any" : p.typeName); + } + return s; + } + + static void emitParamsTo(std::ostream& os, const std::vector& params, + bool hasSelf = false, const std::string& selfType = {}) + { + if (hasSelf) + os << "---@param self " << selfType << "\n"; + + for (std::size_t i = 0; i < params.size(); ++i) + { + const auto& p = params[i]; + std::string pname = p.hint.empty() ? ("p" + std::to_string(i + 1)) : p.hint; + std::string ptype = p.typeName.empty() ? "any" : luaType(p.typeName); + os << "---@param " << pname << " " << ptype << "\n"; + } + } + + std::ostream& out_; + std::ostringstream methodBuf_; + std::string ns_; + std::string curClass_; +}; + +class LuaProxyVisitor : public InspectVisitor +{ +public: + explicit LuaProxyVisitor(std::ostream& out) + : out_(out) + { + } + + void beginNamespace(const NamespaceInspectInfo& ns) override + { + curNs_ = (ns.name == "_G") ? "" : ns.name; + if (!curNs_.empty()) + out_ << "local " << curNs_ << " = {}\n\n"; + } + + void endNamespace(const NamespaceInspectInfo& ns) override + { + if (ns.name != "_G") + out_ << "return " << ns.name << "\n"; + curNs_.clear(); + } + + void visitFreeMember(const NamespaceInspectInfo& ns, const MemberInfo& m) override + { + if (m.kind != MemberKind::Method && m.kind != MemberKind::StaticMethod) + return; + std::string qual = (ns.name == "_G") ? "" : (ns.name + "."); + if (!m.overloads.empty()) + { + out_ << "function " << qual << m.name + << "(" << paramNames(m.overloads[0].params) << ") end\n"; + } + } + + void beginClass(const ClassInspectInfo& cls) override + { + curClass_ = cls.name; + std::string qname = curNs_.empty() ? cls.name : (curNs_ + "." + cls.name); + out_ << qname << " = {}\n"; + out_ << qname << ".__index = " << qname << "\n\n"; + } + + void endClass([[maybe_unused]] const ClassInspectInfo& cls) override + { + curClass_.clear(); + out_ << "\n"; + } + + void visitMember([[maybe_unused]] const ClassInspectInfo& cls, const MemberInfo& m) override + { + if (m.overloads.empty()) + return; + + std::string qname = curNs_.empty() ? curClass_ : (curNs_ + "." + curClass_); + + switch (m.kind) + { + case MemberKind::Constructor: + + out_ << "setmetatable(" << qname << ", {__call = function(t"; + if (!m.overloads[0].params.empty()) + out_ << ", " << paramNames(m.overloads[0].params); + out_ << ")\n return setmetatable({}, t)\nend})\n\n"; + break; + + case MemberKind::Method: + out_ << "function " << qname << ":" << m.name + << "(" << paramNames(m.overloads[0].params) << ") end\n\n"; + break; + + case MemberKind::StaticMethod: + out_ << "function " << qname << "." << m.name + << "(" << paramNames(m.overloads[0].params) << ") end\n\n"; + break; + + case MemberKind::Property: + case MemberKind::ReadOnlyProperty: + case MemberKind::StaticProperty: + case MemberKind::StaticReadOnlyProperty: + + break; + + default: + break; + } + } + +private: + static std::string paramNames(const std::vector& params) + { + std::string s; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i) s += ", "; + s += params[i].hint.empty() ? ("p" + std::to_string(i + 1)) : params[i].hint; + } + return s; + } + + std::ostream& out_; + std::string curNs_; + std::string curClass_; +}; + +inline void inspectPrint(lua_State* L, const char* namespaceName = nullptr, std::ostream& stream = std::cerr) +{ +#if LUABRIDGE_ENABLE_REFLECT + auto ns = inspectNamespace(L, namespaceName); + ConsoleVisitor v(stream); + accept(ns, v); +#else + unused(L, namespaceName, stream); +#endif +} + +} + + +// End File: Source/LuaBridge/Inspect.h + +// Begin File: Source/LuaBridge/List.h + +namespace luabridge { + +template +struct Stack> +{ + using Type = std::list; + + [[nodiscard]] static Result push(lua_State* L, const Type& list) + { +#if LUABRIDGE_SAFE_STACK_CHECKS + if (! lua_checkstack(L, 3)) + return makeErrorCode(ErrorCode::LuaStackOverflow); +#endif + + StackRestore stackRestore(L); + + lua_createtable(L, static_cast(list.size()), 0); - return object; - } + auto it = list.cbegin(); + for (lua_Integer tableIndex = 1; it != list.cend(); ++tableIndex, ++it) + { + lua_pushinteger(L, tableIndex); -private: - Alloc m_alloc; - Dealloc m_dealloc; -}; + auto result = Stack::push(L, *it); + if (! result) + return result; -template -struct container_forwarder -{ - explicit container_forwarder(F f) - : m_func(std::move(f)) - { + lua_settable(L, -3); + } + + stackRestore.reset(); + return {}; } - C operator()(lua_State* L) + [[nodiscard]] static TypeResult get(lua_State* L, int index) { - using FnTraits = function_traits; - using FnArgs = typename FnTraits::argument_types; + if (!lua_istable(L, index)) + return makeErrorCode(ErrorCode::InvalidTypeCast); - C object; - -#if LUABRIDGE_HAS_EXCEPTIONS - try - { -#endif - object = container_constructor::construct(m_func, make_arguments_list(L)); + const StackRestore stackRestore(L); -#if LUABRIDGE_HAS_EXCEPTIONS - } - catch (const std::exception& e) + Type list; + + int absIndex = lua_absindex(L, index); + lua_pushnil(L); + + while (lua_next(L, absIndex) != 0) { - raise_lua_error(L, "%s", e.what()); - } -#endif + auto item = Stack::get(L, -1); + if (! item) + return makeErrorCode(ErrorCode::InvalidTypeCast); - auto result = UserdataSharedHelper::push(L, object); - if (! result) - raise_lua_error(L, "%s", result.error_cstr()); + list.emplace_back(*item); + lua_pop(L, 1); + } - return object; + return list; } -private: - F m_func; + [[nodiscard]] static bool isInstance(lua_State* L, int index) + { + return lua_istable(L, index); + } }; } -} -// End File: Source/LuaBridge/detail/CFunctions.h +// End File: Source/LuaBridge/List.h // Begin File: Source/LuaBridge/detail/Coroutine.h @@ -9306,6 +10635,127 @@ struct Enum // End File: Source/LuaBridge/detail/Enum.h +// Begin File: Source/LuaBridge/detail/FunctionHints.h + +namespace luabridge { + +template +struct FunctionWithHints +{ + using func_type = F; + + F func; + std::vector hints; +}; + +template +[[nodiscard]] auto withHints(F&& func, Names&&... paramNames) -> FunctionWithHints> +{ + return { std::forward(func), { std::string(std::forward(paramNames))... } }; +} + +namespace detail { + +template +struct unwrap_fn_type +{ + using type = T; +}; + +template +struct unwrap_fn_type> +{ + using type = F; +}; + +template +using unwrap_fn_type_t = typename unwrap_fn_type::type; + +template +struct is_function_with_hints : std::false_type +{ +}; + +template +struct is_function_with_hints> : std::true_type +{ +}; + +template +inline static constexpr bool is_function_with_hints_v = is_function_with_hints::value; + +template +struct function_traits> : function_traits +{ +}; + +template +struct is_callable> +{ + static constexpr bool value = is_callable_v; +}; + +template +struct is_const_member_function_pointer> +{ + static constexpr bool value = is_const_member_function_pointer_v; +}; + +template +[[nodiscard]] decltype(auto) get_underlying(F&& f) noexcept +{ + return std::forward(f); +} + +template +[[nodiscard]] F&& get_underlying(FunctionWithHints&& f) noexcept +{ + return std::move(f.func); +} + +template +[[nodiscard]] F& get_underlying(FunctionWithHints& f) noexcept +{ + return f.func; +} + +#if defined(LUABRIDGE_ENABLE_REFLECT) + +template +void reflect_param_type_names_impl(std::vector& result, std::index_sequence) +{ + + ( + [&]() + { + using ParamT = std::tuple_element_t; + + constexpr bool isLuaState = + std::is_pointer_v && + std::is_same_v>, lua_State>; + + if constexpr (!isLuaState) + result.push_back(std::string(typeName>())); + }(), + ...); +} + +template +[[nodiscard]] std::vector reflect_param_type_names() +{ + std::vector result; + reflect_param_type_names_impl(result, std::make_index_sequence>{}); + return result; +} + +#endif + +} +} + + +// End File: Source/LuaBridge/detail/FunctionHints.h + // Begin File: Source/LuaBridge/detail/Globals.h namespace luabridge { @@ -11421,6 +12871,9 @@ class Namespace : public detail::Registrar detail::push_property_readonly(L, name); detail::add_property_setter(L, name, -2); + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); + return *this; } @@ -11436,6 +12889,9 @@ class Namespace : public detail::Registrar detail::push_property_setter(L, std::move(set), name); detail::add_property_setter(L, name, -2); + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); + return *this; } @@ -11448,11 +12904,40 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + + ([&] + { + if constexpr (!detail::is_any_cfunction_pointer_v) + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + using ArgsPack = detail::function_arguments_t; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + + detail::push_function_reflect(L, detail::get_underlying(std::move(functions)), name); + } + else + { + detail::push_function(L, detail::get_underlying(std::move(functions)), name); + } + + } (), ...); +#else ([&] { - detail::push_function(L, std::move(functions), name); + detail::push_function(L, detail::get_underlying(std::move(functions)), name); } (), ...); +#endif } else { @@ -11473,6 +12958,12 @@ class Namespace : public detail::Registrar using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set->entries.push_back(entry); @@ -11484,7 +12975,7 @@ class Namespace : public detail::Registrar ([&] { - detail::push_function(L, std::move(functions), name); + detail::push_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); @@ -11576,6 +13067,9 @@ class Namespace : public detail::Registrar detail::push_property_readonly(L, name); detail::add_property_setter(L, name, -3); + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -2); + return *this; } @@ -11593,6 +13087,9 @@ class Namespace : public detail::Registrar detail::push_class_property_setter(L, std::move(setter), name); detail::add_property_setter(L, name, -3); + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -2); + return *this; } @@ -11611,11 +13108,52 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + + ([&] + { + if constexpr (!detail::is_any_cfunction_pointer_v) + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + if constexpr (detail::is_proxy_member_function_v) + { + using ArgsPack = detail::remove_first_type_t>; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + } + else + { + using ArgsPack = detail::function_arguments_t; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + } + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + + detail::push_member_function_reflect(L, detail::get_underlying(std::move(functions)), name); + } + else + { + + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); + } + + } (), ...); +#else ([&] { - detail::push_member_function(L, std::move(functions), name); + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); } (), ...); +#endif if constexpr (detail::const_functions_count == 1) { @@ -11653,12 +13191,24 @@ class Namespace : public detail::Registrar using ArgsPack = detail::remove_first_type_t>; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } else { using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set_const->entries.push_back(entry); @@ -11675,7 +13225,7 @@ class Namespace : public detail::Registrar if (!detail::is_const_function) return; - detail::push_member_function(L, std::move(functions), name); + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); @@ -11708,12 +13258,24 @@ class Namespace : public detail::Registrar using ArgsPack = detail::remove_first_type_t>; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } else { using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set_nonconst->entries.push_back(entry); @@ -11730,7 +13292,7 @@ class Namespace : public detail::Registrar if (detail::is_const_function) return; - detail::push_member_function(L, std::move(functions), name); + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); @@ -11792,11 +13354,32 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + + ([&] + { + using ArgsPack = detail::function_arguments_t; + + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); + overload_set->entries.push_back(std::move(entry)); + + lua_pushcclosure_x(L, &detail::constructor_placement_proxy, className, 1); + + } (), ...); +#else ([&] { lua_pushcclosure_x(L, &detail::constructor_placement_proxy>, className, 0); } (), ...); +#endif } else { @@ -11810,6 +13393,10 @@ class Namespace : public detail::Registrar detail::OverloadEntry entry; entry.arity = static_cast(detail::function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); +#endif overload_set->entries.push_back(entry); } (), ...); @@ -11846,11 +13433,32 @@ class Namespace : public detail::Registrar { ([&] { - using F = detail::constructor_forwarder; + using InnerF = detail::unwrap_fn_type_t; + using F = detail::constructor_forwarder; - lua_newuserdata_aligned(L, F(std::move(functions))); +#if LUABRIDGE_ENABLE_REFLECT + + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + using ArgsPack = detail::remove_first_type_t>; + detail::OverloadEntry entry; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + } + + lua_newuserdata_aligned(L, F(detail::get_underlying(std::move(functions)))); + lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 2); +#else + lua_newuserdata_aligned(L, F(detail::get_underlying(std::move(functions)))); lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 1); - +#endif } (), ...); } else @@ -11861,8 +13469,10 @@ class Namespace : public detail::Registrar ([&] { + using InnerF = detail::unwrap_fn_type_t; + detail::OverloadEntry entry; - if constexpr (detail::is_any_cfunction_pointer_v) + if constexpr (detail::is_any_cfunction_pointer_v) { entry.arity = -1; entry.checker = nullptr; @@ -11870,9 +13480,15 @@ class Namespace : public detail::Registrar else { - using ArgsPack = detail::remove_first_type_t>; - entry.arity = static_cast(detail::function_arity_excluding_v) - 1; + using ArgsPack = detail::remove_first_type_t>; + entry.arity = static_cast(detail::function_arity_excluding_v) - 1; entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set->entries.push_back(entry); @@ -11884,9 +13500,10 @@ class Namespace : public detail::Registrar ([&] { - using F = detail::constructor_forwarder; + using InnerF = detail::unwrap_fn_type_t; + using F = detail::constructor_forwarder; - lua_newuserdata_aligned(L, F(std::move(functions))); + lua_newuserdata_aligned(L, F(detail::get_underlying(std::move(functions)))); lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 1); lua_rawseti(L, -2, idx++); @@ -12367,6 +13984,9 @@ class Namespace : public detail::Registrar detail::push_property_readonly(L, name); detail::add_property_setter(L, name, -2); + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); + return *this; } @@ -12389,6 +14009,9 @@ class Namespace : public detail::Registrar detail::push_property_setter(L, std::move(setter), name); detail::add_property_setter(L, name, -2); + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); + return *this; } @@ -12401,11 +14024,41 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + ([&] { - detail::push_function(L, std::move(functions), name); + if constexpr (!detail::is_any_cfunction_pointer_v) + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + using ArgsPack = detail::function_arguments_t; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + + detail::push_function_reflect(L, detail::get_underlying(std::move(functions)), name); + } + else + { + + detail::push_function(L, detail::get_underlying(std::move(functions)), name); + } + + } (), ...); +#else + ([&] + { + detail::push_function(L, detail::get_underlying(std::move(functions)), name); } (), ...); +#endif } else { @@ -12426,6 +14079,12 @@ class Namespace : public detail::Registrar using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set->entries.push_back(entry); @@ -12437,7 +14096,7 @@ class Namespace : public detail::Registrar ([&] { - detail::push_function(L, std::move(functions), name); + detail::push_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); diff --git a/Images/benchmarks.png b/Images/benchmarks.png index f015a46a..50009c8b 100644 Binary files a/Images/benchmarks.png and b/Images/benchmarks.png differ diff --git a/Manual.md b/Manual.md index e9f74774..ec640145 100644 --- a/Manual.md +++ b/Manual.md @@ -33,6 +33,7 @@ Contents * [2.4 - Property Member Proxies](#24---property-member-proxies) * [2.5 - Function Member Proxies](#25---function-member-proxies) * [2.5.1 - Function Overloading](#251---function-overloading) + * [2.5.2 - Parameter Name Hints](#252---parameter-name-hints) * [2.6 - Constructors](#26---constructors) * [2.6.1 - Constructor Proxies](#261---constructor-proxies) * [2.6.2 - Constructor Factories](#262---constructor-factories) @@ -44,12 +45,6 @@ Contents * [2.8 - Lua Stack](#28---lua-stack) * [2.8.1 - Enums](#281---enums) * [2.8.2 - lua_State](#282---lua_state) - * [2.9 - C++20 Coroutine Integration](#29---c20-coroutine-integration) - * [2.9.1 - CppCoroutine\ - Generators callable from Lua](#291---cppcoroutiner----generators-callable-from-lua) - * [2.9.2 - Accepting Arguments](#292---accepting-arguments) - * [2.9.3 - Class Coroutines - Static and Member](#293---class-coroutines----static-and-member) - * [2.9.4 - LuaCoroutine - Awaiting a Lua Thread from C++](#294---luacoroutine----awaiting-a-lua-thread-from-c) - * [2.9.5 - Limitations](#295---limitations) * [3 - Passing Objects](#3---passing-objects) @@ -74,16 +69,31 @@ Contents * [4.3.2 - Class LuaException](#432---class-luaexception) * [4.3.3 - Calling with Error Handlers](#433---calling-with-error-handlers) * [4.4 - Wrapping C++ Callables](#44---wrapping-c-callables) + * [4.4.1 - LuaFunction\](#441---luafunctionsignature) + * [4.5 - C++20 Coroutine Integration](#45---c20-coroutine-integration) + * [4.5.1 - CppCoroutine\ - Generators callable from Lua](#451---cppcoroutiner----generators-callable-from-lua) + * [4.5.2 - Accepting Arguments](#452---accepting-arguments) + * [4.5.3 - Class Coroutines - Static and Member](#453---class-coroutines----static-and-member) + * [4.5.4 - LuaCoroutine - Awaiting a Lua Thread from C++](#454---luacoroutine----awaiting-a-lua-thread-from-c) + * [4.5.5 - Limitations](#455---limitations) * [5 - Security](#5---security) -* [6 - Configuration](#6---configuration) +* [6 - Inspecting Registrations](#6---inspecting-registrations) - * [6.1 - LUABRIDGE_SAFE_STACK_CHECKS](#61---luabridge-safe-stack-checks) - * [6.2 - LUABRIDGE_STRICT_STACK_CONVERSIONS](#62---luabridge-strict-stack-conversions) - * [6.3 - LUABRIDGE_SAFE_LUA_C_EXCEPTION_HANDLING](#63---luabridge-safe-c-exception-handling) - * [6.4 - LUABRIDGE_RAISE_UNREGISTERED_CLASS_USAGE](#64---luabridge-raise-unregistered-class-usage) - * [6.5 - LUABRIDGE_HAS_CXX20_COROUTINES / LUABRIDGE_DISABLE_CXX20_COROUTINES](#65---luabridge-has-cxx20-coroutines--luabridge-disable-cxx20-coroutines) + * [6.1 - inspect\ and inspectNamespace](#61---inspectT-and-inspectnamespace) + * [6.2 - Visitor API](#62---visitor-api) + * [6.3 - ConsoleVisitor](#63---consolevisitor) + * [6.4 - Enabling Full Type Information (LUABRIDGE_ENABLE_REFLECT)](#64---enabling-full-type-information-luabridge_enable_reflect) + +* [7 - Configuration](#7---configuration) + + * [7.1 - LUABRIDGE_SAFE_STACK_CHECKS](#71---luabridge-safe-stack-checks) + * [7.2 - LUABRIDGE_STRICT_STACK_CONVERSIONS](#72---luabridge-strict-stack-conversions) + * [7.3 - LUABRIDGE_SAFE_LUA_C_EXCEPTION_HANDLING](#73---luabridge-safe-c-exception-handling) + * [7.4 - LUABRIDGE_RAISE_UNREGISTERED_CLASS_USAGE](#74---luabridge-raise-unregistered-class-usage) + * [7.5 - LUABRIDGE_HAS_CXX20_COROUTINES / LUABRIDGE_DISABLE_CXX20_COROUTINES](#75---luabridge-has-cxx20-coroutines--luabridge-disable-cxx20-coroutines) + * [7.6 - LUABRIDGE_ENABLE_REFLECT](#76---luabridge-enable-reflect) * [Appendix - API Reference](#appendix---api-reference) @@ -659,6 +669,40 @@ It's possible to mix lambdas, function pointers and member functions in overload Special attention needs to be given to the order (priority) of the overloads, based on the number and type of the arguments. Better to place first the overloads that can be called more frequently, and putting "stronger" types first: for example when having an overload taking an `int` and an overload taking `float`, as lua is not able to distinguish between them properly (until lua 5.4) the first overload will always be called. +### 2.5.2 - Parameter Name Hints + +When `LUABRIDGE_ENABLE_REFLECT` is defined (see [7.6](#76---luabridge-enable-reflect)), LuaBridge captures the C++ type names of function parameters at compile time. You can additionally attach human-readable **parameter name hints** to any function using `luabridge::withHints`. These names are stored alongside the function and become available through the [Inspection API](#6---inspecting-registrations). + +```cpp +#define LUABRIDGE_ENABLE_REFLECT +#include +#include // source headers only; omit when using the amalgama + +struct Enemy +{ + void attack(Enemy* target, float damage); + void move(float x, float y, float z); +}; + +luabridge::getGlobalNamespace(L) + .beginNamespace("game") + .beginClass("Enemy") + .addFunction("attack", luabridge::withHints(&Enemy::attack, "target", "damage")) + .addFunction("move", luabridge::withHints(&Enemy::move, "x", "y", "z")) + .endClass() + .endNamespace(); +``` + +`withHints` is transparent: it forwards all function-trait queries to the wrapped function, so it can be used anywhere a plain function pointer is accepted — including overloaded registrations: + +```cpp +.addFunction("rotate", + luabridge::withHints(luabridge::overload(&Vec::rotate), "degrees"), + luabridge::withHints(luabridge::overload(&Vec::rotate), "quaternion")) +``` + +When `LUABRIDGE_ENABLE_REFLECT` is **not** defined, `withHints` is still accepted by the compiler and correctly strips the wrapper — the hints are simply ignored at runtime so there is no overhead. + 2.6 - Constructors ------------------ @@ -1252,181 +1296,6 @@ When the script calls `useStateAndArgs`, it passes only the integer and string p The same is applicable for properties. -2.9 - C++20 Coroutine Integration ----------------------------------- - -LuaBridge3 provides first-class interoperability between C++20 coroutines and Lua coroutines, available when compiling with C++20 or later and Lua 5.2+ (requires `lua_yieldk`). The feature is guarded by `LUABRIDGE_HAS_CXX20_COROUTINES`, which is detected automatically and can be suppressed with `LUABRIDGE_DISABLE_CXX20_COROUTINES`. - -> **Note:** C++20 coroutine integration is not supported on Lua 5.1, LuaJIT, or Luau (those targets lack a public `lua_yieldk` equivalent). - -### 2.9.1 - CppCoroutine\ - Generators callable from Lua - -`luabridge::CppCoroutine` is a coroutine return type that bridges C++20 coroutines with Lua's `coroutine.wrap` / `coroutine.resume` API. A function returning `CppCoroutine` can use `co_yield` to suspend and pass a value back to Lua, and `co_return` to finish and return a final value. - -Register via `Namespace::addCoroutine`: - -```cpp -luabridge::getGlobalNamespace(L) - .addCoroutine("range", [](int from, int to) -> luabridge::CppCoroutine - { - for (int i = from; i <= to; ++i) - co_yield i; - co_return -1; // sentinel value when the range is exhausted - }); -``` - -From Lua, use `coroutine.wrap` to create a callable iterator: - -```lua -local gen = coroutine.wrap(range) -local v = gen(1, 5) -- first call passes arguments; yields 1 -while v ~= -1 do - print(v) -- 1, 2, 3, 4, 5 - v = gen() -- subsequent calls resume without arguments -end -``` - -`CppCoroutine` is also supported for coroutines that produce no values: - -```cpp -.addCoroutine("doWork", []() -> luabridge::CppCoroutine -{ - performStep1(); - co_return; -}); -``` - -An abandoned coroutine (one that goes out of scope in Lua without being fully consumed) is automatically cleaned up by the Lua garbage collector - no manual resource management is needed. - -### 2.9.2 - Accepting Arguments - -The factory lambda receives the Lua call arguments on first invocation. A `lua_State*` parameter, if present, must be the **first** parameter and receives the running Lua thread: - -```cpp -.addCoroutine("adder", [](int a, int b) -> luabridge::CppCoroutine -{ - co_yield a + b; // first resume yields the sum - co_return a * b; // second resume returns the product -}); -``` - -```lua -local f = coroutine.wrap(adder) -print(f(3, 4)) -- 7 (yield: 3+4) -print(f()) -- 12 (return: 3*4) -``` - -Multiple independent instances of the same coroutine factory can run concurrently - each call to `coroutine.wrap(name)` creates a separate C++ coroutine frame: - -```lua -local a = coroutine.wrap(adder) -local b = coroutine.wrap(adder) -a(1, 2) -- independent from b -b(10, 20) -``` - -### 2.9.3 - Class Coroutines - Static and Member - -Coroutines can be attached directly to a registered class using `addStaticCoroutine` and `addCoroutine`. - -**Static coroutines** behave identically to namespace-level coroutines but live in the class's static table. The factory requires no object argument: - -```cpp -luabridge::getGlobalNamespace(L) - .beginClass("Counter") - .addStaticCoroutine("range", [](int from, int count) -> luabridge::CppCoroutine - { - for (int i = 0; i < count; ++i) - co_yield from + i; - co_return -1; - }) - .endClass(); -``` - -```lua -local f = coroutine.wrap(Counter.range) -print(f(5, 3)) -- 5 (first yield) -print(f()) -- 6 -print(f()) -- 7 -print(f()) -- -1 (done) -``` - -**Member coroutines** bind a coroutine factory to individual class instances. The factory's **first argument must be `T*` or `const T*`** - LuaBridge passes the Lua object as that argument automatically: - -```cpp -.beginClass("Counter") - .addCoroutine("generate", [](Counter* obj, int n) -> luabridge::CppCoroutine - { - for (int i = 0; i < n; ++i) - { - co_yield obj->value; - obj->increment(); - } - co_return -1; - }) -.endClass(); -``` - -```lua -local obj = Counter() -local f = coroutine.wrap(Counter.generate) -print(f(obj, 3)) -- 0 (obj.value before first increment) -print(f()) -- 1 -print(f()) -- 2 -print(f()) -- -1 (done) -``` - -**Const vs non-const:** a factory that takes `const T*` as its first argument is registered as a const method - accessible on both const and non-const objects (it appears in both the const and non-const class tables). A factory taking `T*` is registered as a non-const method and is accessible on non-const objects only. - -```cpp -// Accessible on const and non-const objects: -.addCoroutine("peek", [](const Counter* obj) -> luabridge::CppCoroutine -{ - co_yield obj->value; - co_return obj->value * 2; -}) - -// Accessible on non-const objects only: -.addCoroutine("pop", [](Counter* obj) -> luabridge::CppCoroutine -{ - co_yield obj->value--; - co_return obj->value; -}) -``` - -### 2.9.4 - LuaCoroutine - Awaiting a Lua Thread from C++ - -`luabridge::LuaCoroutine` is an awaitable that can be used inside a `CppCoroutine` body to resume a child Lua thread synchronously. It runs the child thread to its first yield or return and gives back the status and the number of values the child left on its stack: - -```cpp -.addCoroutine("driver", [](lua_State* L) -> luabridge::CppCoroutine -{ - // Spawn a child Lua thread and anchor it in the registry so the GC - // doesn't collect it while we hold a pointer to it. - lua_State* child = lua_newthread(L); - int ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops thread from L's stack - - lua_getglobal(child, "luaGenerator"); - - // Resume the child synchronously; suspends this C++ coroutine until done. - auto [status, nresults] = co_await luabridge::LuaCoroutine{ child, L }; - - int value = (nresults > 0) ? static_cast(lua_tointeger(child, -nresults)) : 0; - - luaL_unref(L, LUA_REGISTRYINDEX, ref); - co_return value; -}); -``` - -`LuaCoroutine` always completes synchronously (no external event loop is required). The `status` field contains `LUA_YIELD` if the child yielded or `LUA_OK` if it returned normally. - -### 2.9.5 - Limitations - -* **Lua version:** Requires Lua 5.2+ (`lua_yieldk`). Not supported on Lua 5.1, LuaJIT, or Luau. -* **C++ version:** Requires C++20 (``). Non-coroutine features continue to work under C++17. -* **Multi-value yield:** `co_yield` sends exactly one value per suspension. Use `std::tuple` or a struct if multiple values are needed. -* **Thread safety:** Coroutine frames must be driven from a single OS thread. - 3 - Passing Objects =================== @@ -2021,6 +1890,181 @@ if (result) `LuaFunction` supports the same `call`, `callWithHandler`, and `isValid` interface as a `LuaRef`. The wrapped `LuaRef` is accessible via `ref()`. +4.5 - C++20 Coroutine Integration +---------------------------------- + +LuaBridge3 provides first-class interoperability between C++20 coroutines and Lua coroutines, available when compiling with C++20 or later and Lua 5.2+ (requires `lua_yieldk`). The feature is guarded by `LUABRIDGE_HAS_CXX20_COROUTINES`, which is detected automatically and can be suppressed with `LUABRIDGE_DISABLE_CXX20_COROUTINES`. + +> **Note:** C++20 coroutine integration is not supported on Lua 5.1, LuaJIT, or Luau (those targets lack a public `lua_yieldk` equivalent). + +### 4.5.1 - CppCoroutine\ - Generators callable from Lua + +`luabridge::CppCoroutine` is a coroutine return type that bridges C++20 coroutines with Lua's `coroutine.wrap` / `coroutine.resume` API. A function returning `CppCoroutine` can use `co_yield` to suspend and pass a value back to Lua, and `co_return` to finish and return a final value. + +Register via `Namespace::addCoroutine`: + +```cpp +luabridge::getGlobalNamespace(L) + .addCoroutine("range", [](int from, int to) -> luabridge::CppCoroutine + { + for (int i = from; i <= to; ++i) + co_yield i; + co_return -1; // sentinel value when the range is exhausted + }); +``` + +From Lua, use `coroutine.wrap` to create a callable iterator: + +```lua +local gen = coroutine.wrap(range) +local v = gen(1, 5) -- first call passes arguments; yields 1 +while v ~= -1 do + print(v) -- 1, 2, 3, 4, 5 + v = gen() -- subsequent calls resume without arguments +end +``` + +`CppCoroutine` is also supported for coroutines that produce no values: + +```cpp +.addCoroutine("doWork", []() -> luabridge::CppCoroutine +{ + performStep1(); + co_return; +}); +``` + +An abandoned coroutine (one that goes out of scope in Lua without being fully consumed) is automatically cleaned up by the Lua garbage collector - no manual resource management is needed. + +### 4.5.2 - Accepting Arguments + +The factory lambda receives the Lua call arguments on first invocation. A `lua_State*` parameter, if present, must be the **first** parameter and receives the running Lua thread: + +```cpp +.addCoroutine("adder", [](int a, int b) -> luabridge::CppCoroutine +{ + co_yield a + b; // first resume yields the sum + co_return a * b; // second resume returns the product +}); +``` + +```lua +local f = coroutine.wrap(adder) +print(f(3, 4)) -- 7 (yield: 3+4) +print(f()) -- 12 (return: 3*4) +``` + +Multiple independent instances of the same coroutine factory can run concurrently - each call to `coroutine.wrap(name)` creates a separate C++ coroutine frame: + +```lua +local a = coroutine.wrap(adder) +local b = coroutine.wrap(adder) +a(1, 2) -- independent from b +b(10, 20) +``` + +### 4.5.3 - Class Coroutines - Static and Member + +Coroutines can be attached directly to a registered class using `addStaticCoroutine` and `addCoroutine`. + +**Static coroutines** behave identically to namespace-level coroutines but live in the class's static table. The factory requires no object argument: + +```cpp +luabridge::getGlobalNamespace(L) + .beginClass("Counter") + .addStaticCoroutine("range", [](int from, int count) -> luabridge::CppCoroutine + { + for (int i = 0; i < count; ++i) + co_yield from + i; + co_return -1; + }) + .endClass(); +``` + +```lua +local f = coroutine.wrap(Counter.range) +print(f(5, 3)) -- 5 (first yield) +print(f()) -- 6 +print(f()) -- 7 +print(f()) -- -1 (done) +``` + +**Member coroutines** bind a coroutine factory to individual class instances. The factory's **first argument must be `T*` or `const T*`** - LuaBridge passes the Lua object as that argument automatically: + +```cpp +.beginClass("Counter") + .addCoroutine("generate", [](Counter* obj, int n) -> luabridge::CppCoroutine + { + for (int i = 0; i < n; ++i) + { + co_yield obj->value; + obj->increment(); + } + co_return -1; + }) +.endClass(); +``` + +```lua +local obj = Counter() +local f = coroutine.wrap(Counter.generate) +print(f(obj, 3)) -- 0 (obj.value before first increment) +print(f()) -- 1 +print(f()) -- 2 +print(f()) -- -1 (done) +``` + +**Const vs non-const:** a factory that takes `const T*` as its first argument is registered as a const method - accessible on both const and non-const objects (it appears in both the const and non-const class tables). A factory taking `T*` is registered as a non-const method and is accessible on non-const objects only. + +```cpp +// Accessible on const and non-const objects: +.addCoroutine("peek", [](const Counter* obj) -> luabridge::CppCoroutine +{ + co_yield obj->value; + co_return obj->value * 2; +}) + +// Accessible on non-const objects only: +.addCoroutine("pop", [](Counter* obj) -> luabridge::CppCoroutine +{ + co_yield obj->value--; + co_return obj->value; +}) +``` + +### 4.5.4 - LuaCoroutine - Awaiting a Lua Thread from C++ + +`luabridge::LuaCoroutine` is an awaitable that can be used inside a `CppCoroutine` body to resume a child Lua thread synchronously. It runs the child thread to its first yield or return and gives back the status and the number of values the child left on its stack: + +```cpp +.addCoroutine("driver", [](lua_State* L) -> luabridge::CppCoroutine +{ + // Spawn a child Lua thread and anchor it in the registry so the GC + // doesn't collect it while we hold a pointer to it. + lua_State* child = lua_newthread(L); + int ref = luaL_ref(L, LUA_REGISTRYINDEX); // pops thread from L's stack + + lua_getglobal(child, "luaGenerator"); + + // Resume the child synchronously; suspends this C++ coroutine until done. + auto [status, nresults] = co_await luabridge::LuaCoroutine{ child, L }; + + int value = (nresults > 0) ? static_cast(lua_tointeger(child, -nresults)) : 0; + + luaL_unref(L, LUA_REGISTRYINDEX, ref); + co_return value; +}); +``` + +`LuaCoroutine` always completes synchronously (no external event loop is required). The `status` field contains `LUA_YIELD` if the child yielded or `LUA_OK` if it returned normally. + +### 4.5.5 - Limitations + +* **Lua version:** Requires Lua 5.2+ (`lua_yieldk`). Not supported on Lua 5.1, LuaJIT, or Luau. +* **C++ version:** Requires C++20 (``). Non-coroutine features continue to work under C++17. +* **Multi-value yield:** `co_yield` sends exactly one value per suspension. Use `std::tuple` or a struct if multiple values are needed. +* **Thread safety:** Coroutine frames must be driven from a single OS thread. + 5 - Security ============ @@ -2060,12 +2104,151 @@ luabridge::getGlobalNamespace (L) .endNamespace () ``` -6 - Configuration +6 - Inspecting Registrations +============================ + +LuaBridge3 can introspect its own registration tables at runtime, making it straightforward to generate documentation, build IDE auto-complete databases, produce TypeScript type declarations, or validate that a binding matches an expected schema. + +The inspection API lives in the **optional** header ``. When using the individual source headers, it must be included separately after ``. When using the single-file amalgama (`Distribution/LuaBridge/LuaBridge.h`), `Inspect.h` is already embedded and no separate include is needed. + +Define `LUABRIDGE_ENABLE_REFLECT` before any LuaBridge header to enable C++ type-name capture (see [7.6](#76---luabridge-enable-reflect)). The inspection API works without it — type names will simply be empty strings. + +```cpp +// Source headers: include Inspect.h separately (optional) +#define LUABRIDGE_ENABLE_REFLECT // optional: enables type-name capture +#include +#include + +// Amalgama: Inspect.h is already embedded — no separate include needed +#define LUABRIDGE_ENABLE_REFLECT // optional: enables type-name capture +#include +``` + +6.1 - inspect\ and inspectNamespace +---------------------------------------- + +Two top-level free functions collect inspection data without traversing any Lua tables from user code: + +**`inspect(L)`** — returns a `ClassInspectInfo` for the single registered class `T`. Returns an empty struct if `T` has not been registered. + +```cpp +auto info = luabridge::inspect(L); +std::cout << info.name << "\n"; // "Enemy" +for (const auto& m : info.members) + std::cout << " " << m.name << "\n"; +``` + +**`inspectNamespace(L, name)`** — returns a `NamespaceInspectInfo` for the given namespace. Pass `nullptr` or an empty string to inspect the global namespace `_G`. Dotted paths are supported. + +```cpp +auto ns = luabridge::inspectNamespace(L, "game"); // top-level namespace +auto sub = luabridge::inspectNamespace(L, "game.util"); // nested namespace +auto g = luabridge::inspectNamespace(L); // global namespace (_G) +``` + +The returned structures are plain data — they are independent copies that do not hold references to the Lua state: + +| Type | Fields | +|------|--------| +| `NamespaceInspectInfo` | `name`, `freeMembers`, `classes`, `subNamespaces` | +| `ClassInspectInfo` | `name`, `baseClasses`, `members` | +| `MemberInfo` | `name`, `kind` (`MemberKind`), `overloads` | +| `OverloadInfo` | `returnType`, `params` (`vector`), `isConst` | +| `ParamInfo` | `typeName`, `hint` | + +`MemberKind` is an enum with the following values: + +```cpp +enum class MemberKind +{ + Method, + StaticMethod, + Property, + ReadOnlyProperty, + StaticProperty, + StaticReadOnlyProperty, + Constructor, + Metamethod, +}; +``` + +`returnType` and `typeName` are populated only when `LUABRIDGE_ENABLE_REFLECT` is defined. `hint` is populated when the function was registered with `luabridge::withHints` (see [2.5.2](#252---parameter-name-hints)). + +6.2 - Visitor API +------------------ + +For namespace-wide traversal, a visitor-pattern API is provided. Derive from `InspectVisitor` and override only the callbacks you need: + +```cpp +class InspectVisitor +{ +public: + virtual void beginNamespace(const NamespaceInspectInfo& ns) {} + virtual void endNamespace (const NamespaceInspectInfo& ns) {} + virtual void visitFreeMember(const NamespaceInspectInfo& ns, const MemberInfo& m) {} + + virtual void beginClass(const ClassInspectInfo& cls) {} + virtual void endClass (const ClassInspectInfo& cls) {} + virtual void visitMember(const ClassInspectInfo& cls, const MemberInfo& m) {} +}; +``` + +Drive a visitor over a pre-collected `NamespaceInspectInfo` using `accept`, or use the convenience wrapper `inspectAccept` that collects and visits in one call: + +```cpp +// Option A: collect once, visit multiple times +auto ns = luabridge::inspectNamespace(L, "game"); +MyVisitor v; +luabridge::accept(ns, v); + +// Option B: collect and visit in a single call +luabridge::inspectAccept(L, "game", v); +``` + +The traversal order for `accept` is: +1. `beginNamespace` +2. `visitFreeMember` for each free function / namespace-level property +3. Recursive traversal of each class (begin → members → end) +4. Recursive traversal of each sub-namespace +5. `endNamespace` + +6.3 - ConsoleVisitor +--------------------- + +LuaBridge provides a ready-made visitor, `ConsoleVisitor`, that prints a TypeScript-style declaration to any `std::ostream` (defaults to `std::cerr`): + +```cpp +luabridge::ConsoleVisitor printer(std::cout); +luabridge::inspectAccept(L, "game", printer); +``` + +Example output for a `game` namespace containing an `Enemy` class: + +``` +namespace game { + class Enemy { + constructor(p1: any); + attack(target: Enemy, damage: float): void; + move(x: float, y: float, z: float): void; + readonly hp: int; + static create(p1: float, p2: float): Enemy; + } +} +``` + +Type names and parameter names are filled in when `LUABRIDGE_ENABLE_REFLECT` is active and `withHints` was used during registration. Without reflection the output uses `any` for unknown types and `p1`, `p2`, … as placeholder parameter names. + +6.4 - Enabling Full Type Information (LUABRIDGE_ENABLE_REFLECT) +---------------------------------------------------------------- + +See [7.6 - LUABRIDGE_ENABLE_REFLECT](#76---luabridge-enable-reflect) for the compile-time flag that activates C++ type-name capture. When the flag is off, the inspection API still works — it just reports empty strings for type names. The `withHints` parameter name hints work regardless of the flag. + +7 - Configuration ================= LuaBridge3 exposes several compile-time configuration macros. Each macro can be overridden by defining it **before** including any LuaBridge header, or by passing it as a compiler flag (e.g. `-DLUABRIDGE_SAFE_STACK_CHECKS=0`). -6.1 - LUABRIDGE_SAFE_STACK_CHECKS +7.1 - LUABRIDGE_SAFE_STACK_CHECKS ---------------------------------- **Default: `1` (enabled)** @@ -2079,7 +2262,7 @@ Disable this flag only when you are certain that the Lua stack will never overfl #include ``` -6.2 - LUABRIDGE_STRICT_STACK_CONVERSIONS +7.2 - LUABRIDGE_STRICT_STACK_CONVERSIONS ----------------------------------------- **Default: `0` (disabled)** @@ -2117,7 +2300,7 @@ lua_pushboolean (L, 1); auto r = luabridge::Stack::get (L, -1); // ok: true ``` -6.3 - LUABRIDGE_SAFE_LUA_C_EXCEPTION_HANDLING +7.3 - LUABRIDGE_SAFE_LUA_C_EXCEPTION_HANDLING ----------------------------------------------- **Default: `0` (disabled). Only meaningful when `LUABRIDGE_HAS_EXCEPTIONS` is `1`.** @@ -2133,7 +2316,7 @@ Enable this flag only if you are compiling Lua as C (not as C++), have exception > **Warning:** Enabling this flag introduces a small performance overhead on every registered CFunction call through the library. -6.4 - LUABRIDGE_RAISE_UNREGISTERED_CLASS_USAGE +7.4 - LUABRIDGE_RAISE_UNREGISTERED_CLASS_USAGE ------------------------------------------------ **Default: `1` when exceptions are enabled, `0` otherwise.** @@ -2147,7 +2330,7 @@ Override the default when you need fine-grained control: #include ``` -6.5 - LUABRIDGE_HAS_CXX20_COROUTINES / LUABRIDGE_DISABLE_CXX20_COROUTINES +7.5 - LUABRIDGE_HAS_CXX20_COROUTINES / LUABRIDGE_DISABLE_CXX20_COROUTINES -------------------------------------------------------------------------- **`LUABRIDGE_HAS_CXX20_COROUTINES` - auto-detected, override allowed** @@ -2171,6 +2354,32 @@ You can also override the detection result explicitly: Attempting to use coroutine integration on Lua 5.1, LuaJIT, or Luau will emit a compile-time `#error` unless `LUABRIDGE_DISABLE_COROUTINE_INTEGRATION` is also defined. +7.6 - LUABRIDGE_ENABLE_REFLECT +------------------------------- + +**Default: not defined (disabled)** + +When defined, LuaBridge captures the C++ type names of function parameters and return types at compile time using `typeid`. This information is stored alongside the function in the Lua registry and is retrieved by the [Inspection API](#6---inspecting-registrations) to populate `OverloadInfo::returnType` and `ParamInfo::typeName`. + +Enable reflection by defining the macro **before** including any LuaBridge header: + +```cpp +// Source headers +#define LUABRIDGE_ENABLE_REFLECT +#include +#include // optional: only needed when using the inspection API + +// Amalgama (Inspect.h is already embedded — no separate include needed) +#define LUABRIDGE_ENABLE_REFLECT +#include +``` + +> **Note:** `Inspect.h` does **not** require `LUABRIDGE_ENABLE_REFLECT` — it can always be included. Without the macro the inspection API still works; it just reports empty strings for type names. You may also use `withHints` (see [2.5.2](#252---parameter-name-hints)) without including `Inspect.h`. + +When `LUABRIDGE_ENABLE_REFLECT` is not defined: +* `withHints` parameter name hints are ignored and are not available through inspection. +* `OverloadInfo::returnType` and `ParamInfo::typeName` are empty strings. + Appendix - API Reference ======================== @@ -2610,3 +2819,109 @@ int lua_resume_x(lua_State* L, lua_State* from, int nargs, int* nresults = nullp /// Returns true if the current C function can yield via lua_yieldk. bool lua_isyieldable_x(lua_State* L); ``` + +Registration Inspection (``, enhanced by `LUABRIDGE_ENABLE_REFLECT`) +--------------------------------------------------------------------------------------- + +```cpp +/// Kind of a registered member. +enum class MemberKind +{ + Method, + StaticMethod, + Property, + ReadOnlyProperty, + StaticProperty, + StaticReadOnlyProperty, + Constructor, + Metamethod, +}; + +/// Type and optional name hint for one Lua-visible parameter of an overload. +struct ParamInfo +{ + std::string typeName; ///< C++ type name (e.g. "float", "int", "MyClass"). Empty if unavailable. + std::string hint; ///< Optional user-provided name (e.g. "damage"). Empty if not provided. +}; + +/// Type information for one overload of a registered function or constructor. +struct OverloadInfo +{ + std::string returnType; ///< C++ return type name. Empty if unavailable. + std::vector params; ///< One entry per Lua-visible parameter. + bool isConst = false; ///< True when this is a const member function. +}; + +/// Information about one registered member (method, property, constructor, …). +struct MemberInfo +{ + std::string name; + MemberKind kind = MemberKind::Method; + std::vector overloads; ///< At least 1 entry; may contain empty OverloadInfo. +}; + +/// Inspection result for one registered class. +struct ClassInspectInfo +{ + std::string name; + std::vector baseClasses; ///< Names of all registered ancestor classes. + std::vector members; + + void accept(InspectVisitor& v) const; +}; + +/// Inspection result for a namespace (may contain classes and sub-namespaces). +struct NamespaceInspectInfo +{ + std::string name; + std::vector freeMembers; ///< Free functions and namespace-level properties. + std::vector classes; + std::vector subNamespaces; + + void accept(InspectVisitor& v) const; +}; + +/// Visitor interface for traversing an inspection result tree. +class InspectVisitor +{ +public: + virtual void beginNamespace(const NamespaceInspectInfo& ns) {} + virtual void endNamespace (const NamespaceInspectInfo& ns) {} + virtual void visitFreeMember(const NamespaceInspectInfo& ns, const MemberInfo& m) {} + + virtual void beginClass(const ClassInspectInfo& cls) {} + virtual void endClass (const ClassInspectInfo& cls) {} + virtual void visitMember(const ClassInspectInfo& cls, const MemberInfo& m) {} +}; + +/// Built-in visitor that emits TypeScript-style pseudo-declarations to an ostream. +class ConsoleVisitor : public InspectVisitor +{ +public: + explicit ConsoleVisitor(std::ostream& out = std::cerr); + // overrides all six InspectVisitor methods +}; + +/// Inspect a single registered class by its C++ type. +/// Returns an empty ClassInspectInfo if T is not registered. +template +[[nodiscard]] ClassInspectInfo inspect(lua_State* L); + +/// Inspect a namespace by name. Pass nullptr or "" for the global namespace. +/// Supports dotted paths (e.g. "Outer.Inner"). +/// Returns an empty NamespaceInspectInfo if the name is not found. +[[nodiscard]] NamespaceInspectInfo inspectNamespace(lua_State* L, const char* namespaceName = nullptr); + +/// Traverse a NamespaceInspectInfo with a visitor. +void accept(const NamespaceInspectInfo& ns, InspectVisitor& v); +void accept(const ClassInspectInfo& cls, InspectVisitor& v); + +/// Collect inspection data for a namespace and drive a visitor over it (convenience wrapper). +void inspectAccept(lua_State* L, const char* namespaceName, InspectVisitor& visitor); + +/// Attach Lua parameter name hints to a function for use with addFunction / addStaticFunction. +/// Works with and without LUABRIDGE_ENABLE_REFLECT. Hints are stored at runtime; type capture +/// requires the macro. +template +[[nodiscard]] FunctionWithHints> withHints(F&& func, Names&&... paramNames); +``` diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 4503fafe..54271565 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required (VERSION 3.10) set (LUABRIDGE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/LuaBridge/Array.h ${CMAKE_CURRENT_SOURCE_DIR}/LuaBridge/Dump.h + ${CMAKE_CURRENT_SOURCE_DIR}/LuaBridge/Inspect.h ${CMAKE_CURRENT_SOURCE_DIR}/LuaBridge/List.h ${CMAKE_CURRENT_SOURCE_DIR}/LuaBridge/LuaBridge.h ${CMAKE_CURRENT_SOURCE_DIR}/LuaBridge/Map.h diff --git a/Source/LuaBridge/Inspect.h b/Source/LuaBridge/Inspect.h new file mode 100644 index 00000000..acf5013f --- /dev/null +++ b/Source/LuaBridge/Inspect.h @@ -0,0 +1,1268 @@ +// https://github.com/kunitoki/LuaBridge3 +// Copyright 2026, kunitoki +// SPDX-License-Identifier: MIT + +#pragma once + +#include "detail/CFunctions.h" +#include "detail/ClassInfo.h" +#include "detail/LuaHelpers.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace luabridge { + +//================================================================================================= +// Forward declarations + +struct NamespaceInspectInfo; +struct ClassInspectInfo; +class InspectVisitor; + +void accept(const NamespaceInspectInfo& ns, InspectVisitor& v); +void accept(const ClassInspectInfo& cls, InspectVisitor& v); + +//================================================================================================= +/** + * @brief Kind of a registered member. + */ +enum class MemberKind +{ + Method, + StaticMethod, + Property, + ReadOnlyProperty, + StaticProperty, + StaticReadOnlyProperty, + Constructor, + Metamethod, +}; + +//================================================================================================= +/** + * @brief Type and optional name hint for one Lua-visible parameter of an overload. + */ +struct ParamInfo +{ + std::string typeName; ///< C++ type name (e.g. "float", "int", "MyClass"). Empty if unavailable. + std::string hint; ///< Optional user-provided name (e.g. "damage"). Empty if not provided. +}; + +//================================================================================================= +/** + * @brief Type information for one overload of a registered function or constructor. + */ +struct OverloadInfo +{ + std::string returnType; ///< C++ return type name. Empty if unavailable. + std::vector params; ///< One entry per Lua-visible parameter. + bool isConst = false; ///< True when this is a const member function. +}; + +//================================================================================================= +/** + * @brief Information about one registered member (method, property, constructor, …). + */ +struct MemberInfo +{ + std::string name; + MemberKind kind = MemberKind::Method; + std::vector overloads; ///< At least 1 entry; may contain empty OverloadInfo. +}; + +//================================================================================================= +/** + * @brief Inspection result for one registered class. + */ +struct ClassInspectInfo +{ + std::string name; + std::vector baseClasses; ///< Names of all registered ancestor classes. + std::vector members; + + void accept(InspectVisitor& v) const; +}; + +//================================================================================================= +/** + * @brief Inspection result for a namespace (may contain classes and sub-namespaces). + */ +struct NamespaceInspectInfo +{ + std::string name; + std::vector freeMembers; ///< Free functions and namespace-level properties. + std::vector classes; + std::vector subNamespaces; + + void accept(InspectVisitor& v) const; +}; + +//================================================================================================= +/** + * @brief Visitor interface for traversing an inspection result tree. + * + * Override the methods you care about. Default implementations are no-ops so you only + * need to override what you need. + */ +class InspectVisitor +{ +public: + virtual ~InspectVisitor() = default; + + virtual void beginNamespace(const NamespaceInspectInfo& /*ns*/) {} + virtual void endNamespace(const NamespaceInspectInfo& /*ns*/) {} + virtual void visitFreeMember(const NamespaceInspectInfo& /*ns*/, const MemberInfo& /*m*/) {} + + virtual void beginClass(const ClassInspectInfo& /*cls*/) {} + virtual void endClass(const ClassInspectInfo& /*cls*/) {} + virtual void visitMember(const ClassInspectInfo& /*cls*/, const MemberInfo& /*m*/) {} +}; + +//================================================================================================= +/** + * @brief Traverse a namespace inspection result, calling visitor callbacks in order. + */ +inline void accept(const NamespaceInspectInfo& ns, InspectVisitor& v) +{ + v.beginNamespace(ns); + for (const auto& m : ns.freeMembers) + v.visitFreeMember(ns, m); + for (const auto& cls : ns.classes) + accept(cls, v); + for (const auto& sub : ns.subNamespaces) + accept(sub, v); + v.endNamespace(ns); +} + +inline void accept(const ClassInspectInfo& cls, InspectVisitor& v) +{ + v.beginClass(cls); + for (const auto& m : cls.members) + v.visitMember(cls, m); + v.endClass(cls); +} + +inline void ClassInspectInfo::accept(InspectVisitor& v) const { luabridge::accept(*this, v); } +inline void NamespaceInspectInfo::accept(InspectVisitor& v) const { luabridge::accept(*this, v); } + +//================================================================================================= +// Internal helpers + +namespace detail { + +// Read all string keys from a Lua table at tableIdx into a set +inline std::set collectTableKeys(lua_State* L, int tableIdx) +{ + std::set keys; + tableIdx = lua_absindex(L, tableIdx); + lua_pushnil(L); + while (lua_next(L, tableIdx)) + { + if (lua_type(L, -2) == LUA_TSTRING) + keys.insert(lua_tostring(L, -2)); + lua_pop(L, 1); // pop value, keep key + } + return keys; +} + +// Strip a "const " prefix from a type name string +inline std::string stripConst(const char* name) +{ + std::string s = name ? name : ""; + if (s.substr(0, 6) == "const ") + s = s.substr(6); + return s; +} + +// Return the overload infos for a function/closure sitting at funcIdx. +// If there's no OverloadSet upvalue (bare cfunction), returns 1 entry with empty type strings. +inline std::vector getOverloadInfos(lua_State* L, int funcIdx) +{ + funcIdx = lua_absindex(L, funcIdx); + + if (!lua_isfunction(L, funcIdx)) + return { OverloadInfo{} }; + + // Check upvalue 1 for an OverloadSet userdata identified by its magic cookie. + // This handles two layouts: + // Multi-overload: upvalue[1]=OverloadSet, upvalue[2]=table of functions + // Single-overload REFLECT: upvalue[1]=OverloadSet, upvalue[2]=function data directly + const char* uv1name = lua_getupvalue(L, funcIdx, 1); + if (uv1name == nullptr) + { + // No upvalues — bare cfunction, no metadata available + return { OverloadInfo{} }; + } + + OverloadSet* overload_set = nullptr; + if (isfulluserdata(L, -1)) + { + auto* candidate = align(lua_touserdata(L, -1)); + if (candidate && candidate->magic == OverloadSet::kMagic) + overload_set = candidate; + } + lua_pop(L, 1); // pop upvalue 1 + + if (!overload_set || overload_set->entries.empty()) + return { OverloadInfo{} }; + + std::vector result; + result.reserve(overload_set->entries.size()); + + for (const auto& entry : overload_set->entries) + { + OverloadInfo info; + +#if LUABRIDGE_ENABLE_REFLECT + info.returnType = entry.returnType; + for (std::size_t i = 0; i < entry.paramTypes.size(); ++i) + { + ParamInfo p; + p.typeName = entry.paramTypes[i]; + if (i < entry.paramHints.size()) + p.hint = entry.paramHints[i]; + info.params.push_back(std::move(p)); + } +#else + // Without LUABRIDGE_ENABLE_REFLECT: emit synthetic params based on arity only + if (entry.arity >= 0) + { + for (int i = 0; i < entry.arity; ++i) + info.params.push_back(ParamInfo{}); + } +#endif + + result.push_back(std::move(info)); + } + + return result; +} + +// Returns true if the table at tableIdx is a class static table (has a metatable with getClassKey). +// Also leaves the class metatable (mt) on the stack top if true (caller must pop it). +// Leaves nothing extra on the stack if false. +inline bool isClassStaticTable(lua_State* L, int tableIdx) +{ + tableIdx = lua_absindex(L, tableIdx); + + if (!lua_getmetatable(L, tableIdx)) // always works from C regardless of __metatable + return false; + + lua_rawgetp_x(L, -1, getClassKey()); // mt[getClassKey()] = cl ? + bool result = lua_istable(L, -1); + lua_pop(L, 1); // pop cl / nil + + if (!result) + { + lua_pop(L, 1); // pop mt + return false; + } + + // Leave mt on stack (caller needs it) + return true; +} + +// Inspect a class from its static table (st at stIdx). +// Assumes isClassStaticTable(L, stIdx) returned true and left mt on the stack. +inline ClassInspectInfo inspectClassFromStaticTable(lua_State* L, int stIdx) +{ + stIdx = lua_absindex(L, stIdx); + + // mt is on top of the stack (left there by isClassStaticTable) + int mtIdx = lua_absindex(L, -1); + + // Get the class table cl = mt[getClassKey()] + lua_rawgetp_x(L, mtIdx, getClassKey()); + int clIdx = lua_absindex(L, -1); + + ClassInspectInfo cls; + + // 1. Class name + lua_rawgetp_x(L, clIdx, getTypeKey()); + if (lua_isstring(L, -1)) + cls.name = stripConst(lua_tostring(L, -1)); + lua_pop(L, 1); + + // 2. Base classes from parent list + lua_rawgetp_x(L, clIdx, getParentKey()); + if (lua_istable(L, -1)) + { + int parIdx = lua_absindex(L, -1); + int len = get_length(L, parIdx); + for (int i = 1; i <= len; ++i) + { + lua_rawgeti(L, parIdx, i); + lua_rawgetp_x(L, -1, getTypeKey()); + if (lua_isstring(L, -1)) + { + std::string baseName = stripConst(lua_tostring(L, -1)); + if (!baseName.empty() && baseName != cls.name) + cls.baseClasses.push_back(std::move(baseName)); + } + lua_pop(L, 2); // pop typename + parent mt + } + } + lua_pop(L, 1); // pop parent list / nil + + // 3. Collect instance property names + std::set instPropget; + lua_rawgetp_x(L, clIdx, getPropgetKey()); + if (lua_istable(L, -1)) + instPropget = collectTableKeys(L, -1); + lua_pop(L, 1); + + // Collect instance property names that have a real (non-readonly-sentinel) setter + std::set instPropsetReal; + lua_rawgetp_x(L, clIdx, getPropsetKey()); + if (lua_istable(L, -1)) + { + int psIdx = lua_absindex(L, -1); + for (const auto& k : instPropget) + { + lua_getfield(L, psIdx, k.c_str()); + if (lua_isfunction(L, -1)) + { + lua_CFunction fn = lua_tocfunction(L, -1); + if (fn != &detail::read_only_error) + instPropsetReal.insert(k); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // Collect instance property type names (may be absent if not registered with type info) + std::map instPropTypes; + lua_rawgetp_x(L, clIdx, getPropTypeKey()); + if (lua_istable(L, -1)) + { + int ptIdx = lua_absindex(L, -1); + for (const auto& k : instPropget) + { + lua_getfield(L, ptIdx, k.c_str()); + if (lua_isstring(L, -1)) + instPropTypes[k] = lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // 4. Emit instance properties + for (const auto& propName : instPropget) + { + MemberInfo m; + m.name = propName; + m.kind = instPropsetReal.count(propName) ? MemberKind::Property : MemberKind::ReadOnlyProperty; + OverloadInfo ov; + auto it = instPropTypes.find(propName); + if (it != instPropTypes.end()) + ov.returnType = it->second; + m.overloads.push_back(std::move(ov)); + cls.members.push_back(std::move(m)); + } + + // 5. Iterate class table for instance methods / metamethods + static const std::set skipKeys{ "__index", "__newindex", "__metatable" }; + + lua_pushnil(L); + while (lua_next(L, clIdx)) + { + if (lua_type(L, -2) == LUA_TSTRING) + { + std::string key = lua_tostring(L, -2); + if (!skipKeys.count(key) && !instPropget.count(key)) + { + if (lua_isfunction(L, -1) || lua_isuserdata(L, -1)) + { + MemberInfo m; + m.name = key; + m.kind = (key.size() >= 2 && key[0] == '_' && key[1] == '_') + ? MemberKind::Metamethod + : MemberKind::Method; + m.overloads = getOverloadInfos(L, -1); + cls.members.push_back(std::move(m)); + } + } + } + lua_pop(L, 1); // pop value + } + + // 6. Collect static property names + std::set stPropget; + lua_rawgetp_x(L, mtIdx, getPropgetKey()); + if (lua_istable(L, -1)) + stPropget = collectTableKeys(L, -1); + lua_pop(L, 1); + + // Collect static property names that have a real (non-readonly-sentinel) setter + std::set stPropsetReal; + lua_rawgetp_x(L, mtIdx, getPropsetKey()); + if (lua_istable(L, -1)) + { + int psIdx = lua_absindex(L, -1); + for (const auto& k : stPropget) + { + lua_getfield(L, psIdx, k.c_str()); + if (lua_isfunction(L, -1)) + { + lua_CFunction fn = lua_tocfunction(L, -1); + if (fn != &detail::read_only_error) + stPropsetReal.insert(k); + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // Collect static property type names + std::map stPropTypes; + lua_rawgetp_x(L, mtIdx, getPropTypeKey()); + if (lua_istable(L, -1)) + { + int ptIdx = lua_absindex(L, -1); + for (const auto& k : stPropget) + { + lua_getfield(L, ptIdx, k.c_str()); + if (lua_isstring(L, -1)) + stPropTypes[k] = lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + // 7. Emit static properties + for (const auto& propName : stPropget) + { + MemberInfo m; + m.name = propName; + m.kind = stPropsetReal.count(propName) ? MemberKind::StaticProperty : MemberKind::StaticReadOnlyProperty; + OverloadInfo ov; + auto it = stPropTypes.find(propName); + if (it != stPropTypes.end()) + ov.returnType = it->second; + m.overloads.push_back(std::move(ov)); + cls.members.push_back(std::move(m)); + } + + // 8. Iterate mt for static methods + constructor + static const std::set staticSkipKeys{ "__index", "__newindex", "__metatable" }; + + lua_rawgetp_x(L, mtIdx, getClassKey()); // just to see mt's string keys, iterate mt directly + lua_pop(L, 1); + + lua_pushnil(L); + while (lua_next(L, mtIdx)) + { + if (lua_type(L, -2) == LUA_TSTRING) + { + std::string key = lua_tostring(L, -2); + if (!staticSkipKeys.count(key) && !stPropget.count(key)) + { + if (lua_isfunction(L, -1) || lua_isuserdata(L, -1)) + { + MemberInfo m; + m.name = key; + if (key == "__call") + m.kind = MemberKind::Constructor; + else if (key.size() >= 2 && key[0] == '_' && key[1] == '_') + m.kind = MemberKind::Metamethod; + else + m.kind = MemberKind::StaticMethod; + m.overloads = getOverloadInfos(L, -1); + cls.members.push_back(std::move(m)); + } + } + } + lua_pop(L, 1); + } + + lua_pop(L, 2); // pop cl, mt + return cls; +} + +// Inspect a namespace table at nsIdx +inline NamespaceInspectInfo inspectNamespaceTable(lua_State* L, int nsIdx, std::string name); + +inline NamespaceInspectInfo inspectNamespaceTable(lua_State* L, int nsIdx, std::string name) +{ + nsIdx = lua_absindex(L, nsIdx); + NamespaceInspectInfo info; + info.name = std::move(name); + + // Namespace-level properties (stored directly on the namespace table via getPropgetKey) + std::set nsPropget; + lua_rawgetp_x(L, nsIdx, getPropgetKey()); + if (lua_istable(L, -1)) + nsPropget = collectTableKeys(L, -1); + lua_pop(L, 1); + + // Get the propset table so we can check each setter individually + int nsPropsetTableIdx = 0; + lua_rawgetp_x(L, nsIdx, getPropsetKey()); + if (lua_istable(L, -1)) + nsPropsetTableIdx = lua_absindex(L, -1); + // leave propset table on stack; we pop it after the loop + + // Collect namespace property type names + std::map nsPropTypes; + lua_rawgetp_x(L, nsIdx, getPropTypeKey()); + if (lua_istable(L, -1)) + { + int ptIdx = lua_absindex(L, -1); + for (const auto& k : nsPropget) + { + lua_getfield(L, ptIdx, k.c_str()); + if (lua_isstring(L, -1)) + nsPropTypes[k] = lua_tostring(L, -1); + lua_pop(L, 1); + } + } + lua_pop(L, 1); // pop proptype table or nil + + for (const auto& propName : nsPropget) + { + MemberInfo m; + m.name = propName; + + // A getter-only namespace property still gets a read_only_error sentinel in propset. + // Detect the sentinel by comparing the setter's underlying C function pointer. + bool isReadOnly = true; + if (nsPropsetTableIdx) + { + lua_getfield(L, nsPropsetTableIdx, propName.c_str()); + if (lua_isfunction(L, -1)) + { + lua_CFunction fn = lua_tocfunction(L, -1); + isReadOnly = (fn == &detail::read_only_error); + } + lua_pop(L, 1); + } + + m.kind = isReadOnly ? MemberKind::ReadOnlyProperty : MemberKind::Property; + OverloadInfo ov; + auto it = nsPropTypes.find(propName); + if (it != nsPropTypes.end()) + ov.returnType = it->second; + m.overloads.push_back(std::move(ov)); + info.freeMembers.push_back(std::move(m)); + } + + lua_pop(L, 1); // pop propset table (or nil) + + // Iterate namespace table entries + static const std::set nsSkipKeys{ "__index", "__newindex", "__metatable", "__gc" }; + + lua_pushnil(L); + while (lua_next(L, nsIdx)) + { + if (lua_type(L, -2) != LUA_TSTRING) + { + lua_pop(L, 1); + continue; + } + + std::string key = lua_tostring(L, -2); + + if (nsSkipKeys.count(key) || nsPropget.count(key)) + { + lua_pop(L, 1); + continue; + } + + if (lua_istable(L, -1)) + { + int valIdx = lua_absindex(L, -1); + + if (isClassStaticTable(L, valIdx)) + { + // mt is now on top of the stack (left by isClassStaticTable) + auto cls = inspectClassFromStaticTable(L, valIdx); + // inspectClassFromStaticTable pops mt and cl + info.classes.push_back(std::move(cls)); + } + else + { + auto sub = inspectNamespaceTable(L, valIdx, key); + info.subNamespaces.push_back(std::move(sub)); + } + } + else if (lua_isfunction(L, -1)) + { + if (!nsPropget.count(key)) + { + MemberInfo m; + m.name = key; + m.kind = MemberKind::Method; + m.overloads = getOverloadInfos(L, -1); + info.freeMembers.push_back(std::move(m)); + } + } + + lua_pop(L, 1); // pop value + } + + return info; +} + +} // namespace detail + +//================================================================================================= +/** + * @brief Inspect a single registered class by its C++ type. + * + * Uses the compile-time registry key — no namespace traversal required. + * + * @returns Inspection result, or an empty ClassInspectInfo if T is not registered. + */ +template +[[nodiscard]] ClassInspectInfo inspect(lua_State* L) +{ +#if LUABRIDGE_ENABLE_REFLECT + // Load the class table (cl) directly from the Lua registry + lua_rawgetp_x(L, LUA_REGISTRYINDEX, detail::getClassRegistryKey()); + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + return {}; + } + // Stack: ..., cl + + // cl[getStaticKey()] = st (the internal static table that acts as a metatable) + // NOTE: this is NOT the user-visible ns["ClassName"] table; it is the metatable of that table. + // isClassStaticTable() cannot be used here because st itself has no metatable. + lua_rawgetp_x(L, -1, detail::getStaticKey()); + if (!lua_istable(L, -1)) + { + lua_pop(L, 2); + return {}; + } + // Stack: ..., cl, st + + // Verify st actually belongs to a registered class: st[getClassKey()] must be a table + lua_rawgetp_x(L, -1, detail::getClassKey()); + const bool isClass = lua_istable(L, -1); + lua_pop(L, 1); + + if (!isClass) + { + lua_pop(L, 2); + return {}; + } + // Stack: ..., cl, st (st is at top — inspectClassFromStaticTable reads it as "mt") + + ClassInspectInfo result = detail::inspectClassFromStaticTable(L, lua_gettop(L)); + // inspectClassFromStaticTable pops st (used as mt) and the cl it fetched from st[getClassKey()] + // Stack: ..., cl + lua_pop(L, 1); // pop cl + return result; +#else + unused(L); + return {}; +#endif +} + +//================================================================================================= +/** + * @brief Inspect a namespace by name. + * + * @param namespaceName The global name of the namespace (e.g. "MyNS"), or nullptr / "" for the + * global namespace. Supports dotted paths ("Outer.Inner"). + * + * @returns Inspection result, or an empty NamespaceInspectInfo if the name is not found. + */ +[[nodiscard]] inline NamespaceInspectInfo inspectNamespace(lua_State* L, const char* namespaceName = nullptr) +{ +#if LUABRIDGE_ENABLE_REFLECT + if (namespaceName == nullptr || namespaceName[0] == '\0') + { + lua_getglobal(L, "_G"); + } + else + { + // Support dotted paths by navigating component by component + std::string path = namespaceName; + std::string first; + std::string::size_type dot = path.find('.'); + if (dot == std::string::npos) + first = path; + else + first = path.substr(0, dot); + + lua_getglobal(L, first.c_str()); + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + return {}; + } + + while (dot != std::string::npos) + { + path = path.substr(dot + 1); + dot = path.find('.'); + std::string component = (dot == std::string::npos) ? path : path.substr(0, dot); + lua_getfield(L, -1, component.c_str()); + lua_remove(L, -2); // remove previous table + if (!lua_istable(L, -1)) + { + lua_pop(L, 1); + return {}; + } + } + } + + // Use only the leaf component of a dotted path as the display name + std::string label; + if (namespaceName && namespaceName[0]) + { + std::string_view sv(namespaceName); + auto dot = sv.rfind('.'); + label = std::string(dot == std::string_view::npos ? sv : sv.substr(dot + 1)); + } + else + { + label = "_G"; + } + auto result = detail::inspectNamespaceTable(L, -1, label); + lua_pop(L, 1); + return result; +#else + unused(L, namespaceName); + return {}; +#endif +} + +//================================================================================================= +/** + * @brief Collect inspection data for a namespace and drive a visitor over it. + */ +inline void inspectAccept(lua_State* L, const char* namespaceName, InspectVisitor& visitor) +{ +#if LUABRIDGE_ENABLE_REFLECT + auto ns = inspectNamespace(L, namespaceName); + accept(ns, visitor); +#else + unused(L, namespaceName, visitor); +#endif +} + +//================================================================================================= +/** + * @brief Visitor that emits TypeScript-style pseudo-code to an ostream. + * + * Useful for quick debugging and documentation. + * + * Example output: + * @code + * namespace MyNS { + * class Foo extends Base { + * constructor(); + * method(p1: float): void; + * readonly value: int; + * static create(): Foo; + * } + * } + * @endcode + */ +class ConsoleVisitor : public InspectVisitor +{ +public: + explicit ConsoleVisitor(std::ostream& out = std::cerr) + : out_(out) + { + } + + void beginNamespace(const NamespaceInspectInfo& ns) override + { + indent(); + out_ << "namespace " << ns.name << " {\n"; + ++depth_; + } + + void endNamespace([[maybe_unused]] const NamespaceInspectInfo& ns) override + { + --depth_; + indent(); + out_ << "}\n"; + } + + void visitFreeMember([[maybe_unused]] const NamespaceInspectInfo& ns, const MemberInfo& m) override + { + emitMember(m, ""); + } + + void beginClass(const ClassInspectInfo& cls) override + { + indent(); + out_ << "class " << cls.name; + if (!cls.baseClasses.empty()) + { + out_ << " extends "; + for (std::size_t i = 0; i < cls.baseClasses.size(); ++i) + { + if (i) out_ << ", "; + out_ << cls.baseClasses[i]; + } + } + out_ << " {\n"; + ++depth_; + } + + void endClass([[maybe_unused]] const ClassInspectInfo& cls) override + { + --depth_; + indent(); + out_ << "}\n"; + } + + void visitMember(const ClassInspectInfo& cls, const MemberInfo& m) override + { + emitMember(m, cls.name); + } + +private: + void indent() const + { + for (int i = 0; i < depth_; ++i) + out_ << " "; + } + + static std::string paramStr(const OverloadInfo& ov) + { + std::string s; + for (std::size_t i = 0; i < ov.params.size(); ++i) + { + if (i) s += ", "; + const auto& p = ov.params[i]; + s += p.hint.empty() ? ("p" + std::to_string(i + 1)) : p.hint; + s += ": "; + s += p.typeName.empty() ? "any" : p.typeName; + } + return s; + } + + static std::string retStr(const OverloadInfo& ov, const std::string& constructorClass = "") + { + if (!constructorClass.empty()) + return constructorClass; + return ov.returnType.empty() ? "any" : ov.returnType; + } + + void emitMember(const MemberInfo& m, [[maybe_unused]] const std::string& className) const + { + switch (m.kind) + { + case MemberKind::Property: + indent(); + out_ << m.name << ": any;\n"; + break; + + case MemberKind::ReadOnlyProperty: + indent(); + out_ << "readonly " << m.name << ": any;\n"; + break; + + case MemberKind::StaticProperty: + indent(); + out_ << "static " << m.name << ": any;\n"; + break; + + case MemberKind::StaticReadOnlyProperty: + indent(); + out_ << "static readonly " << m.name << ": any;\n"; + break; + + case MemberKind::Metamethod: + // Skip metamethods in TypeScript-style output + break; + + case MemberKind::Constructor: + for (const auto& ov : m.overloads) + { + indent(); + out_ << "constructor(" << paramStr(ov) << ");\n"; + } + break; + + case MemberKind::Method: + case MemberKind::StaticMethod: + { + const bool isStatic = (m.kind == MemberKind::StaticMethod); + for (std::size_t i = 0; i < m.overloads.size(); ++i) + { + indent(); + if (isStatic) out_ << "static "; + out_ << m.name << "(" << paramStr(m.overloads[i]) << "): " << retStr(m.overloads[i]) << ";\n"; + } + break; + } + + default: + break; + } + } + + std::ostream& out_; + int depth_ = 0; +}; + +//================================================================================================= +/** + * @brief Visitor that emits LuaLS (Lua Language Server) / LuaCATS annotation stubs. + * + * Suitable for generating `.lua` type stub files consumed by lua-language-server. + * + * @see https://luals.github.io/wiki/annotations/ + */ +class LuaLSVisitor : public InspectVisitor +{ +public: + explicit LuaLSVisitor(std::ostream& out) + : out_(out) + { + } + + void beginNamespace(const NamespaceInspectInfo& ns) override + { + ns_ = ns.name == "_G" ? "" : ns.name; + } + + void endNamespace([[maybe_unused]] const NamespaceInspectInfo& ns) override {} + + void visitFreeMember(const NamespaceInspectInfo& ns, const MemberInfo& m) override + { + if (m.kind != MemberKind::Method && m.kind != MemberKind::StaticMethod) + return; // properties not directly emittable as LuaLS annotations easily + + for (const auto& ov : m.overloads) + { + emitParamsTo(out_, ov.params); + if (!ov.returnType.empty() && ov.returnType != "void") + out_ << "---@return " << luaType(ov.returnType) << "\n"; + } + + std::string qual = ns_.empty() ? "" : (ns_ + "."); + out_ << "function " << qual << m.name << "("; + if (!m.overloads.empty()) + out_ << paramNames(m.overloads[0].params); + out_ << ") end\n\n"; + } + + void beginClass(const ClassInspectInfo& cls) override + { + curClass_ = cls.name; + out_ << "---@class " << qualifiedName(cls.name); + if (!cls.baseClasses.empty()) + { + out_ << " : " << cls.baseClasses[0]; + for (std::size_t i = 1; i < cls.baseClasses.size(); ++i) + out_ << ", " << cls.baseClasses[i]; + } + out_ << "\n"; + methodBuf_.str({}); + methodBuf_.clear(); + } + + void endClass([[maybe_unused]] const ClassInspectInfo& cls) override + { + out_ << "local " << curClass_ << " = {}\n"; + const std::string methods = methodBuf_.str(); + if (!methods.empty()) + out_ << "\n" << methods; + else + out_ << "\n"; + curClass_.clear(); + } + + void visitMember(const ClassInspectInfo& cls, const MemberInfo& m) override + { + auto propType = [&]() -> std::string { + if (!m.overloads.empty() && !m.overloads[0].returnType.empty()) + return luaType(m.overloads[0].returnType); + return "any"; + }; + + switch (m.kind) + { + case MemberKind::Property: + out_ << "---@field " << m.name << " " << propType() << "\n"; + break; + + case MemberKind::ReadOnlyProperty: + out_ << "---@field " << m.name << " " << propType() << " # readonly\n"; + break; + + case MemberKind::StaticProperty: + out_ << "---@field " << m.name << " " << propType() << "\n"; + break; + + case MemberKind::StaticReadOnlyProperty: + out_ << "---@field " << m.name << " " << propType() << " # readonly\n"; + break; + + case MemberKind::Constructor: + { + // Represent the constructor as @overload annotations before the local declaration. + // LuaBridge constructors are called as ClassName(args), not ClassName.new(args). + std::string qname = qualifiedName(cls.name); + for (const auto& ov : m.overloads) + out_ << "---@overload fun(" << overloadParams(ov.params) << "): " << qname << "\n"; + break; + } + + case MemberKind::Method: + { + std::string qname = qualifiedName(cls.name); + for (std::size_t i = 0; i < m.overloads.size(); ++i) + { + const auto& ov = m.overloads[i]; + if (i == 0) + { + emitParamsTo(methodBuf_, ov.params, /*hasSelf=*/true, qname); + if (!ov.returnType.empty() && ov.returnType != "void") + methodBuf_ << "---@return " << luaType(ov.returnType) << "\n"; + methodBuf_ << "function " << qname << ":" << m.name << "(" << paramNames(ov.params) << ") end\n\n"; + } + else + { + methodBuf_ << "---@overload fun(self: " << qname << ", " << overloadParams(ov.params) << ")" + << (ov.returnType.empty() ? "" : (": " + luaType(ov.returnType))) << "\n"; + } + } + break; + } + + case MemberKind::StaticMethod: + { + std::string qname = qualifiedName(cls.name); + for (std::size_t i = 0; i < m.overloads.size(); ++i) + { + const auto& ov = m.overloads[i]; + if (i == 0) + { + emitParamsTo(methodBuf_, ov.params); + if (!ov.returnType.empty() && ov.returnType != "void") + methodBuf_ << "---@return " << luaType(ov.returnType) << "\n"; + methodBuf_ << "function " << qname << "." << m.name << "(" << paramNames(ov.params) << ") end\n\n"; + } + else + { + methodBuf_ << "---@overload fun(" << overloadParams(ov.params) << ")" + << (ov.returnType.empty() ? "" : (": " + luaType(ov.returnType))) << "\n"; + } + } + break; + } + + default: + break; + } + } + +private: + // Minimal C++ → Lua type name mapping + static std::string luaType(const std::string& cppType) + { + if (cppType == "void") return "nil"; + + if (cppType == "bool") return "boolean"; + + if (cppType == "int" || cppType == "long" || cppType == "short" || + cppType == "unsigned int" || cppType == "unsigned long" || + cppType == "unsigned short" || cppType == "int64_t" || cppType == "uint64_t" || + cppType == "int32_t" || cppType == "uint32_t" || cppType == "size_t") + return "integer"; + + if (cppType == "float" || cppType == "double") return "number"; + + if (cppType == "std::string" || cppType == "const char *" || cppType == "const char*") + return "string"; + + return cppType.empty() ? "any" : cppType; + } + + std::string qualifiedName(const std::string& name) const + { + return ns_.empty() ? name : (ns_ + "." + name); + } + + static std::string paramNames(const std::vector& params) + { + std::string s; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i) s += ", "; + s += params[i].hint.empty() ? ("p" + std::to_string(i + 1)) : params[i].hint; + } + return s; + } + + static std::string overloadParams(const std::vector& params) + { + std::string s; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i) s += ", "; + + const auto& p = params[i]; + std::string pname = p.hint.empty() ? ("p" + std::to_string(i + 1)) : p.hint; + s += pname + ": " + luaType(p.typeName.empty() ? "any" : p.typeName); + } + return s; + } + + static void emitParamsTo(std::ostream& os, const std::vector& params, + bool hasSelf = false, const std::string& selfType = {}) + { + if (hasSelf) + os << "---@param self " << selfType << "\n"; + + for (std::size_t i = 0; i < params.size(); ++i) + { + const auto& p = params[i]; + std::string pname = p.hint.empty() ? ("p" + std::to_string(i + 1)) : p.hint; + std::string ptype = p.typeName.empty() ? "any" : luaType(p.typeName); + os << "---@param " << pname << " " << ptype << "\n"; + } + } + + std::ostream& out_; + std::ostringstream methodBuf_; + std::string ns_; + std::string curClass_; +}; + +//================================================================================================= +/** + * @brief Visitor that emits a real Lua stub file (require()-able, mockable in tests). + * + * Each class is emitted as a module-local table with stub methods. + * Namespace-level functions are emitted as module-level functions. + */ +class LuaProxyVisitor : public InspectVisitor +{ +public: + explicit LuaProxyVisitor(std::ostream& out) + : out_(out) + { + } + + void beginNamespace(const NamespaceInspectInfo& ns) override + { + curNs_ = (ns.name == "_G") ? "" : ns.name; + if (!curNs_.empty()) + out_ << "local " << curNs_ << " = {}\n\n"; + } + + void endNamespace(const NamespaceInspectInfo& ns) override + { + if (ns.name != "_G") + out_ << "return " << ns.name << "\n"; + curNs_.clear(); + } + + void visitFreeMember(const NamespaceInspectInfo& ns, const MemberInfo& m) override + { + if (m.kind != MemberKind::Method && m.kind != MemberKind::StaticMethod) + return; + std::string qual = (ns.name == "_G") ? "" : (ns.name + "."); + if (!m.overloads.empty()) + { + out_ << "function " << qual << m.name + << "(" << paramNames(m.overloads[0].params) << ") end\n"; + } + } + + void beginClass(const ClassInspectInfo& cls) override + { + curClass_ = cls.name; + std::string qname = curNs_.empty() ? cls.name : (curNs_ + "." + cls.name); + out_ << qname << " = {}\n"; + out_ << qname << ".__index = " << qname << "\n\n"; + } + + void endClass([[maybe_unused]] const ClassInspectInfo& cls) override + { + curClass_.clear(); + out_ << "\n"; + } + + void visitMember([[maybe_unused]] const ClassInspectInfo& cls, const MemberInfo& m) override + { + if (m.overloads.empty()) + return; + + std::string qname = curNs_.empty() ? curClass_ : (curNs_ + "." + curClass_); + + switch (m.kind) + { + case MemberKind::Constructor: + // LuaBridge constructors are called as ClassName(args) via __call metamethod. + out_ << "setmetatable(" << qname << ", {__call = function(t"; + if (!m.overloads[0].params.empty()) + out_ << ", " << paramNames(m.overloads[0].params); + out_ << ")\n return setmetatable({}, t)\nend})\n\n"; + break; + + case MemberKind::Method: + out_ << "function " << qname << ":" << m.name + << "(" << paramNames(m.overloads[0].params) << ") end\n\n"; + break; + + case MemberKind::StaticMethod: + out_ << "function " << qname << "." << m.name + << "(" << paramNames(m.overloads[0].params) << ") end\n\n"; + break; + + case MemberKind::Property: + case MemberKind::ReadOnlyProperty: + case MemberKind::StaticProperty: + case MemberKind::StaticReadOnlyProperty: + // Properties are part of the table, no explicit function needed + break; + + default: + break; + } + } + +private: + static std::string paramNames(const std::vector& params) + { + std::string s; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i) s += ", "; + s += params[i].hint.empty() ? ("p" + std::to_string(i + 1)) : params[i].hint; + } + return s; + } + + std::ostream& out_; + std::string curNs_; + std::string curClass_; +}; + +//================================================================================================= +/** + * @brief Print a TypeScript-style inspection of a namespace to an ostream. + * + * Convenience wrapper around ConsoleVisitor. + */ +inline void inspectPrint(lua_State* L, const char* namespaceName = nullptr, std::ostream& stream = std::cerr) +{ +#if LUABRIDGE_ENABLE_REFLECT + auto ns = inspectNamespace(L, namespaceName); + ConsoleVisitor v(stream); + accept(ns, v); +#else + unused(L, namespaceName, stream); +#endif +} + +} // namespace luabridge diff --git a/Source/LuaBridge/LuaBridge.h b/Source/LuaBridge/LuaBridge.h index ede78604..b64370e4 100644 --- a/Source/LuaBridge/LuaBridge.h +++ b/Source/LuaBridge/LuaBridge.h @@ -24,6 +24,7 @@ #include "detail/Expected.h" #include "detail/FlagSet.h" #include "detail/FuncTraits.h" +#include "detail/FunctionHints.h" #include "detail/Globals.h" #include "detail/Invoke.h" #include "detail/Iterator.h" diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index b8621190..0bd474f8 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -1391,6 +1391,38 @@ inline void add_property_getter(lua_State* L, const char* name, int tableIndex) lua_pop(L, 2); // Stack: - } +//================================================================================================= +/** + * @brief Store a property's C++ type name string in a side-table for inspection/tooling. + * + * Creates the side-table (keyed by getPropTypeKey()) on demand inside the table at tableIndex. + * A no-op when typeName is null or empty. + */ +inline void add_property_type(lua_State* L, const char* name, const char* typeName, int tableIndex) +{ +#if LUABRIDGE_SAFE_STACK_CHECKS + luaL_checkstack(L, 3, detail::error_lua_stack_overflow); +#endif + + if (!typeName || typeName[0] == '\0') + return; + + tableIndex = lua_absindex(L, tableIndex); + + lua_rawgetp_x(L, tableIndex, getPropTypeKey()); // Stack: ..., proptype (or nil) + if (lua_isnil(L, -1)) + { + lua_pop(L, 1); + lua_createtable(L, 0, 4); + lua_pushvalue(L, -1); + lua_rawsetp_x(L, tableIndex, getPropTypeKey()); + } + // Stack: ..., proptype + lua_pushstring(L, typeName); + rawsetfield(L, -2, name); + lua_pop(L, 1); // pop proptype +} + //================================================================================================= template @@ -1682,35 +1714,35 @@ struct function * * The member function pointer is in the first upvalue. The class userdata object is at the top of the Lua stack. */ -template +template int invoke_member_function(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); 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))); + const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(UV))); LUABRIDGE_ASSERT(func != nullptr); return function::call(L, *ptr, func); } -template +template int invoke_const_member_function(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); 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))); + const F& func = *static_cast(lua_touserdata(L, lua_upvalueindex(UV))); LUABRIDGE_ASSERT(func != nullptr); return function::call(L, *ptr, func); @@ -1790,14 +1822,14 @@ int invoke_const_member_cfunction(lua_State* L) * * The proxy function pointer (lightuserdata) is in the first upvalue. The class userdata object is at the top of the Lua stack. */ -template +template int invoke_proxy_function(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(lua_islightuserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(lua_islightuserdata(L, lua_upvalueindex(UV))); - auto func = reinterpret_cast(lua_touserdata(L, lua_upvalueindex(1))); + auto func = reinterpret_cast(lua_touserdata(L, lua_upvalueindex(UV))); LUABRIDGE_ASSERT(func != nullptr); return function::call(L, func); @@ -1809,14 +1841,14 @@ int invoke_proxy_function(lua_State* L) * * The proxy std::function (lightuserdata) is in the first upvalue. The class userdata object is at the top of the Lua stack. */ -template +template int invoke_proxy_functor(lua_State* L) { using FnTraits = function_traits>; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); - auto& func = *align>(lua_touserdata(L, lua_upvalueindex(1))); + auto& func = *align>(lua_touserdata(L, lua_upvalueindex(UV))); return function::call(L, func); } @@ -1851,14 +1883,14 @@ inline int invoke_safe_cfunction(lua_State* L) * * The proxy std::function (lightuserdata) is in the first upvalue. The class userdata object will be pushed at the top of the Lua stack. */ -template +template int invoke_proxy_constructor(lua_State* L) { using FnTraits = function_traits; - LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(UV))); - auto& func = *align(lua_touserdata(L, lua_upvalueindex(1))); + auto& func = *align(lua_touserdata(L, lua_upvalueindex(UV))); function::call(L, func); @@ -1897,16 +1929,28 @@ struct OverloadEntry int arity; // -1 for variadic (lua_CFunction): always attempt TypeChecker checker; // nullptr for variadic: skip type pre-checking + +#if LUABRIDGE_ENABLE_REFLECT + std::string returnType; ///< C++ return type name (from detail::typeName) + std::vector paramTypes; ///< C++ parameter type names (excluding lua_State*) + std::vector paramHints; ///< Optional user-provided parameter names (from withHints) +#endif }; /** * @brief C++ storage for all overloads of a function. * * Stored as a Lua full userdata so it is GC'd automatically when the closure is collected. - * The actual function closures are stored separately in a flat Lua table (upvalue 2). + * For multi-overload dispatch: the actual function closures are in a flat Lua table (upvalue 2). + * For single-overload REFLECT: the OverloadSet is upvalue[1] of the actual function closure directly. + * + * The magic field allows getOverloadInfos() to reliably distinguish this from other full-userdata + * upvalues (e.g. functor objects stored in invoke_proxy_functor closures). */ struct OverloadSet { + static constexpr uint32_t kMagic = 0x4C425246u; // 'LBRF' — LuaBridge ReFLect sentinel + uint32_t magic = kMagic; std::vector entries; }; @@ -1965,6 +2009,8 @@ bool overload_type_checker(lua_State* L, int start) * 2. Type check via Stack::isInstance in C++ (no pcall) — skips clearly mismatched overloads. * 3. Only calls lua_pcall for type-matched candidates, eliminating failed pcalls for type mismatches. */ + + template inline int try_overload_functions(lua_State* L) { @@ -2086,6 +2132,35 @@ inline void push_function(lua_State* L, F&& f, const char* debugname) lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 1); } +//================================================================================================= +// REFLECT variants of push_function: OverloadSet is already at stack top (upvalue[1]); +// these push function data as upvalue[2] and create a 2-upvalue closure. + +// Generic function pointer (reflect) +template +inline void push_function_reflect(lua_State* L, ReturnType (*fp)(Params...), const char* debugname) +{ + using FnType = ReturnType (*)(Params...); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +inline void push_function_reflect(lua_State* L, ReturnType (*fp)(Params...) noexcept, const char* debugname) +{ + using FnType = ReturnType (*)(Params...) noexcept; + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +// Callable object / lambda (reflect) +template && !std::is_pointer_v && !std::is_member_function_pointer_v>> +inline void push_function_reflect(lua_State* L, F&& f, const char* debugname) +{ + lua_newuserdata_aligned(L, std::forward(f)); + lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 2); +} + //================================================================================================= // Lua CFunction template @@ -2256,6 +2331,127 @@ void push_member_function(lua_State* L, int (U::*mfp)(lua_State*) const, const c lua_pushcclosure_x(L, &invoke_const_member_cfunction, debugname, 1); } +//================================================================================================= +// REFLECT variants of push_member_function: OverloadSet is already at stack top (upvalue[1]); +// these push function data as upvalue[2] and create a 2-upvalue closure. +// Only non-cfunction types are handled here; is_any_cfunction_pointer_v types skip OverloadSet. + +// Generic function pointer (proxy-style, first arg is T* or T&) +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T*, Params...), const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T&, Params...), const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T*, Params...) noexcept, const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(T&, Params...) noexcept, const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T*, Params...), const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T&, Params...), const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T*, Params...) noexcept, const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (*fp)(const T&, Params...) noexcept, const char* debugname) +{ + using FnType = decltype(fp); + lua_pushlightuserdata(L, reinterpret_cast(fp)); + lua_pushcclosure_x(L, &invoke_proxy_function, debugname, 2); +} + +// True member function pointers (non-const) +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...), const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...) noexcept, const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_member_function, debugname, 2); +} + +// True member function pointers (const) +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...) const, const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 2); +} + +template +void push_member_function_reflect(lua_State* L, ReturnType (U::*mfp)(Params...) const noexcept, const char* debugname) +{ + static_assert(std::is_same_v || std::is_base_of_v); + using F = decltype(mfp); + new (lua_newuserdata_x(L, sizeof(F))) F(mfp); + lua_pushcclosure_x(L, &invoke_const_member_function, debugname, 2); +} + +// Callable object / lambda (reflect) +template && + std::is_object_v && + !std::is_pointer_v && + !std::is_member_function_pointer_v>> +void push_member_function_reflect(lua_State* L, F&& f, const char* debugname) +{ + static_assert(std::is_same_v>>>); + lua_newuserdata_aligned(L, std::forward(f)); + lua_pushcclosure_x(L, &invoke_proxy_functor, debugname, 2); +} + //================================================================================================= /** * @brief @@ -2604,16 +2800,30 @@ struct constructor { static T* construct(const Args& args) { - auto alloc = [](auto&&... args) { return new T(std::forward(args)...); }; - - return std::apply(alloc, args); + if constexpr (std::is_aggregate_v) + { + auto alloc = [](auto&&... args) { return new T{std::forward(args)...}; }; + return std::apply(alloc, args); + } + else + { + auto alloc = [](auto&&... args) { return new T(std::forward(args)...); }; + return std::apply(alloc, args); + } } static T* construct(void* ptr, const Args& args) { - auto alloc = [ptr](auto&&... args) { return new (ptr) T(std::forward(args)...); }; - - return std::apply(alloc, args); + if constexpr (std::is_aggregate_v) + { + auto alloc = [ptr](auto&&... args) { return new (ptr) T{std::forward(args)...}; }; + return std::apply(alloc, args); + } + else + { + auto alloc = [ptr](auto&&... args) { return new (ptr) T(std::forward(args)...); }; + return std::apply(alloc, args); + } } }; diff --git a/Source/LuaBridge/detail/ClassInfo.h b/Source/LuaBridge/detail/ClassInfo.h index ad90f244..603037d5 100644 --- a/Source/LuaBridge/detail/ClassInfo.h +++ b/Source/LuaBridge/detail/ClassInfo.h @@ -146,6 +146,17 @@ template ().find_first_of('.')> return reinterpret_cast(0xdad); } +//================================================================================================= +/** + * @brief The key of a property-type table in a class/namespace metatable. + * + * Maps property names to their C++ return type strings (for inspection/IDE tooling). + */ +[[nodiscard]] inline const void* getPropTypeKey() noexcept +{ + return reinterpret_cast(0x9e7); +} + //================================================================================================= /** * @brief The key of a cast offset table in a derived class metatable. diff --git a/Source/LuaBridge/detail/FuncTraits.h b/Source/LuaBridge/detail/FuncTraits.h index c121bc48..c87af0d4 100644 --- a/Source/LuaBridge/detail/FuncTraits.h +++ b/Source/LuaBridge/detail/FuncTraits.h @@ -547,5 +547,35 @@ struct remove_first_type> template using remove_first_type_t = typename remove_first_type::type; +//================================================================================================= +/** + * @brief Deduces the value type of a property getter. + * + * Handles member object pointers (T C::*), callable getters (member functions, free + * functions, lambdas), and falls back to void for anything else. + */ +template +struct getter_return_type +{ + using type = void; +}; + +// Member object pointer: T C::* (not a function member pointer) +template +struct getter_return_type>> +{ + using type = std::remove_const_t; +}; + +// Callable getter (member function pointer, free function pointer, lambda/functor) +template +struct getter_return_type>> +{ + using type = remove_cvref_t>; +}; + +template +using getter_return_t = typename getter_return_type::type; + } // namespace detail } // namespace luabridge diff --git a/Source/LuaBridge/detail/FunctionHints.h b/Source/LuaBridge/detail/FunctionHints.h new file mode 100644 index 00000000..1ab29d2e --- /dev/null +++ b/Source/LuaBridge/detail/FunctionHints.h @@ -0,0 +1,178 @@ +// https://github.com/kunitoki/LuaBridge3 +// Copyright 2024, kunitoki +// SPDX-License-Identifier: MIT + +#pragma once + +#include "ClassInfo.h" +#include "FuncTraits.h" + +#include +#include +#include + +namespace luabridge { + +//================================================================================================= +/** + * @brief A wrapper that attaches Lua parameter name hints to a function or callable. + * + * Use luabridge::withHints() to create one. Transparently forwards all function trait queries + * to the wrapped function type, so it can be used anywhere a plain function is accepted. + * + * @code + * .addFunction("attack", luabridge::withHints(&Enemy::attack, "target", "damage")) + * @endcode + */ +template +struct FunctionWithHints +{ + using func_type = F; + + F func; + std::vector hints; ///< Optional parameter name hints, one per Lua-visible parameter +}; + +/** + * @brief Attach Lua parameter name hints to a function for use with addFunction / addStaticFunction. + * + * @param func The function, member function pointer, or lambda to wrap. + * @param paramNames Parameter names in the same order as the Lua-visible parameters. + * + * @returns A FunctionWithHints wrapper accepted by all addFunction / addStaticFunction overloads. + */ +template +[[nodiscard]] auto withHints(F&& func, Names&&... paramNames) -> FunctionWithHints> +{ + return { std::forward(func), { std::string(std::forward(paramNames))... } }; +} + +namespace detail { + +//================================================================================================= +// Trait: safely unwrap FunctionWithHints to F, identity otherwise. +// Unlike conditional_t, typename T::func_type, T>, +// this trait never instantiates the ::func_type member for non-FunctionWithHints types. + +template +struct unwrap_fn_type +{ + using type = T; +}; + +template +struct unwrap_fn_type> +{ + using type = F; +}; + +template +using unwrap_fn_type_t = typename unwrap_fn_type::type; + +//================================================================================================= +// Trait: detect FunctionWithHints + +template +struct is_function_with_hints : std::false_type +{ +}; + +template +struct is_function_with_hints> : std::true_type +{ +}; + +template +inline static constexpr bool is_function_with_hints_v = is_function_with_hints::value; + +//================================================================================================= +// Make function_traits> delegate to function_traits +// This makes function_result_t<>, function_arguments_t<>, function_arity_v<>, etc. all work +// transparently through a FunctionWithHints wrapper. + +template +struct function_traits> : function_traits +{ +}; + +//================================================================================================= +// Make is_callable> true when F is callable + +template +struct is_callable> +{ + static constexpr bool value = is_callable_v; +}; + +//================================================================================================= +// Make const-member-pointer detection transparent through FunctionWithHints + +template +struct is_const_member_function_pointer> +{ + static constexpr bool value = is_const_member_function_pointer_v; +}; + +//================================================================================================= +// Helper: unwrap FunctionWithHints to its inner callable, or pass through unchanged. + +template +[[nodiscard]] decltype(auto) get_underlying(F&& f) noexcept +{ + return std::forward(f); +} + +template +[[nodiscard]] F&& get_underlying(FunctionWithHints&& f) noexcept +{ + return std::move(f.func); +} + +template +[[nodiscard]] F& get_underlying(FunctionWithHints& f) noexcept +{ + return f.func; +} + +//================================================================================================= +// Reflection helpers — only compiled when LUABRIDGE_ENABLE_REFLECT is defined. + +#if defined(LUABRIDGE_ENABLE_REFLECT) + +/** + * @brief Build a vector of C++ type name strings from a tuple of types. + * + * lua_State* parameters are filtered out (they are auto-injected by LuaBridge, not + * visible from Lua). + */ +template +void reflect_param_type_names_impl(std::vector& result, std::index_sequence) +{ + // Fold expression over comma operator (C++17): invoke a lambda for each index + ( + [&]() + { + using ParamT = std::tuple_element_t; + + constexpr bool isLuaState = + std::is_pointer_v && + std::is_same_v>, lua_State>; + + if constexpr (!isLuaState) + result.push_back(std::string(typeName>())); + }(), + ...); +} + +template +[[nodiscard]] std::vector reflect_param_type_names() +{ + std::vector result; + reflect_param_type_names_impl(result, std::make_index_sequence>{}); + return result; +} + +#endif // LUABRIDGE_ENABLE_REFLECT + +} // namespace detail +} // namespace luabridge diff --git a/Source/LuaBridge/detail/LuaHelpers.h b/Source/LuaBridge/detail/LuaHelpers.h index daab583b..099aed52 100644 --- a/Source/LuaBridge/detail/LuaHelpers.h +++ b/Source/LuaBridge/detail/LuaHelpers.h @@ -564,6 +564,57 @@ void* lua_newuserdata_aligned(lua_State* L, Args&&... args) return pointer; } +/** + * @brief Portable wrapper for lua_resume that normalises calling convention differences + * across Lua 5.1/LuaJIT (no from, no nresults), 5.2-5.3 (from but no nresults), and 5.4+ (from + nresults). + * + * @param L The coroutine thread to resume. + * @param from The thread doing the resuming (may be nullptr on older Lua). + * @param nargs Number of arguments on L's stack to pass to the resumed function. + * @param nresults Output: number of values on L's stack after resume (yielded or returned). + * For Lua 5.4+, filled directly by lua_resume. For older versions, computed via lua_gettop. + * @returns LUA_OK, LUA_YIELD, or an error code. + */ +inline int lua_resume_x(lua_State* L, lua_State* from, int nargs, int* nresults = nullptr) +{ +#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 + unused(from); + int status = lua_resume(L, nargs); + if (nresults) + *nresults = lua_gettop(L); + return status; +#elif LUABRIDGE_ON_LUAU || LUABRIDGE_ON_RAVI || LUA_VERSION_NUM < 504 + int status = lua_resume(L, from, nargs); + if (nresults) + *nresults = lua_gettop(L); + return status; +#else + int nr = 0; + int status = lua_resume(L, from, nargs, &nr); + if (nresults) + *nresults = nr; + return status; +#endif +} + +/** + * @brief Returns true if the currently running C function can yield via lua_yieldk. + * + * Returns false on Lua 5.1, LuaJIT, and Luau where lua_yieldk is unavailable. + */ +inline bool lua_isyieldable_x(lua_State* L) +{ +#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 || LUABRIDGE_ON_LUAU + unused(L); + return false; +#elif LUA_VERSION_NUM < 503 + unused(L); + return true; // lua_yieldk exists in 5.2; assume yieldable when reached +#else + return lua_isyieldable(L) != 0; +#endif +} + /** * @brief Safe error able to walk backwards for error reporting correctly. */ @@ -702,55 +753,4 @@ bool is_floating_point_representable_by(lua_State* L, int index) return isValid ? is_floating_point_representable_by(value) : false; } -/** - * @brief Portable wrapper for lua_resume that normalises calling convention differences - * across Lua 5.1/LuaJIT (no from, no nresults), 5.2-5.3 (from but no nresults), and 5.4+ (from + nresults). - * - * @param L The coroutine thread to resume. - * @param from The thread doing the resuming (may be nullptr on older Lua). - * @param nargs Number of arguments on L's stack to pass to the resumed function. - * @param nresults Output: number of values on L's stack after resume (yielded or returned). - * For Lua 5.4+, filled directly by lua_resume. For older versions, computed via lua_gettop. - * @returns LUA_OK, LUA_YIELD, or an error code. - */ -inline int lua_resume_x(lua_State* L, lua_State* from, int nargs, int* nresults = nullptr) -{ -#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 - unused(from); - int status = lua_resume(L, nargs); - if (nresults) - *nresults = lua_gettop(L); - return status; -#elif LUABRIDGE_ON_LUAU || LUABRIDGE_ON_RAVI || LUA_VERSION_NUM < 504 - int status = lua_resume(L, from, nargs); - if (nresults) - *nresults = lua_gettop(L); - return status; -#else - int nr = 0; - int status = lua_resume(L, from, nargs, &nr); - if (nresults) - *nresults = nr; - return status; -#endif -} - -/** - * @brief Returns true if the currently running C function can yield via lua_yieldk. - * - * Returns false on Lua 5.1, LuaJIT, and Luau where lua_yieldk is unavailable. - */ -inline bool lua_isyieldable_x(lua_State* L) -{ -#if LUABRIDGE_ON_LUAJIT || LUA_VERSION_NUM == 501 || LUABRIDGE_ON_LUAU - unused(L); - return false; -#elif LUA_VERSION_NUM < 503 - unused(L); - return true; // lua_yieldk exists in 5.2; assume yieldable when reached -#else - return lua_isyieldable(L) != 0; -#endif -} - } // namespace luabridge diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 3dc59f6b..cdc0f60a 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -709,6 +709,9 @@ class Namespace : public detail::Registrar detail::push_property_readonly(L, name); // Stack: co, cl, st, function detail::add_property_setter(L, name, -2); // Stack: co, cl, st + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); // st + return *this; } @@ -724,6 +727,9 @@ class Namespace : public detail::Registrar detail::push_property_setter(L, std::move(set), name); // Stack: co, cl, st, function detail::add_property_setter(L, name, -2); // Stack: co, cl, st + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); // st + return *this; } @@ -745,11 +751,41 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + // Embed 1-entry OverloadSet as upvalue[1] of the actual function closure so + // metadata is accessible at runtime without a wrapper. ([&] { - detail::push_function(L, std::move(functions), name); + if constexpr (!detail::is_any_cfunction_pointer_v) + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + using ArgsPack = detail::function_arguments_t; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + + detail::push_function_reflect(L, detail::get_underlying(std::move(functions)), name); + } + else + { + detail::push_function(L, detail::get_underlying(std::move(functions)), name); + } + + } (), ...); +#else + ([&] + { + detail::push_function(L, detail::get_underlying(std::move(functions)), name); } (), ...); +#endif } else { @@ -770,6 +806,12 @@ class Namespace : public detail::Registrar using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set->entries.push_back(entry); @@ -782,7 +824,7 @@ class Namespace : public detail::Registrar ([&] { - detail::push_function(L, std::move(functions), name); + detail::push_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); @@ -890,6 +932,9 @@ class Namespace : public detail::Registrar detail::push_property_readonly(L, name); // Stack: co, cl, st, function detail::add_property_setter(L, name, -3); // Stack: co, cl, st + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -2); // cl + return *this; } @@ -907,6 +952,9 @@ class Namespace : public detail::Registrar detail::push_class_property_setter(L, std::move(setter), name); // Stack: co, cl, st, setter detail::add_property_setter(L, name, -3); // Stack: co, cl, st + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -2); // cl + return *this; } @@ -934,11 +982,56 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + // Embed 1-entry OverloadSet as upvalue[1] of the actual function closure so + // metadata is accessible at runtime without a wrapper (avoiding C-to-C call chains + // that break luaL_argerror function-name resolution). + ([&] + { + if constexpr (!detail::is_any_cfunction_pointer_v) + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + if constexpr (detail::is_proxy_member_function_v) + { + using ArgsPack = detail::remove_first_type_t>; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + } + else + { + using ArgsPack = detail::function_arguments_t; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + } + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + + // OverloadSet is now at stack top (upvalue[1]). + // push_member_function_reflect pushes function data as upvalue[2]. + detail::push_member_function_reflect(L, detail::get_underlying(std::move(functions)), name); + } + else + { + // Bare cfunction: no type metadata to embed; register directly. + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); + } + + } (), ...); +#else ([&] { - detail::push_member_function(L, std::move(functions), name); + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); } (), ...); +#endif if constexpr (detail::const_functions_count == 1) { @@ -976,12 +1069,24 @@ class Namespace : public detail::Registrar using ArgsPack = detail::remove_first_type_t>; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } else { using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set_const->entries.push_back(entry); @@ -999,7 +1104,7 @@ class Namespace : public detail::Registrar if (!detail::is_const_function) return; - detail::push_member_function(L, std::move(functions), name); + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); @@ -1033,12 +1138,24 @@ class Namespace : public detail::Registrar using ArgsPack = detail::remove_first_type_t>; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } else { using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::member_function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set_nonconst->entries.push_back(entry); @@ -1056,7 +1173,7 @@ class Namespace : public detail::Registrar if (detail::is_const_function) return; - detail::push_member_function(L, std::move(functions), name); + detail::push_member_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); @@ -1158,11 +1275,35 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + // Embed 1-entry OverloadSet as upvalue[1] of the constructor closure. + // constructor_placement_proxy uses no upvalues internally, so upvalue[1] + // is only there for getOverloadInfos() to read metadata. + ([&] + { + using ArgsPack = detail::function_arguments_t; + + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); + overload_set->entries.push_back(std::move(entry)); + + // OverloadSet is at stack top (upvalue[1]); constructor_placement_proxy ignores it. + lua_pushcclosure_x(L, &detail::constructor_placement_proxy, className, 1); + + } (), ...); +#else ([&] { lua_pushcclosure_x(L, &detail::constructor_placement_proxy>, className, 0); } (), ...); +#endif } else { @@ -1176,6 +1317,10 @@ class Namespace : public detail::Registrar detail::OverloadEntry entry; entry.arity = static_cast(detail::function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); +#endif overload_set->entries.push_back(entry); } (), ...); @@ -1222,11 +1367,33 @@ class Namespace : public detail::Registrar { ([&] { - using F = detail::constructor_forwarder; + using InnerF = detail::unwrap_fn_type_t; + using F = detail::constructor_forwarder; - lua_newuserdata_aligned(L, F(std::move(functions))); // Stack: co, cl, st, upvalue +#if LUABRIDGE_ENABLE_REFLECT + // Embed 1-entry OverloadSet as upvalue[1] of the constructor closure so + // metadata is accessible at runtime without a wrapper. + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + // skip void* first arg (placement new destination, not a Lua argument) + using ArgsPack = detail::remove_first_type_t>; + detail::OverloadEntry entry; + entry.arity = -1; + entry.checker = nullptr; + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + } + // OverloadSet is now at stack top (upvalue[1]). + lua_newuserdata_aligned(L, F(detail::get_underlying(std::move(functions)))); // upvalue[2] + lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 2); +#else + lua_newuserdata_aligned(L, F(detail::get_underlying(std::move(functions)))); // Stack: co, cl, st, upvalue lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 1); // Stack: co, cl, st, function - +#endif } (), ...); } else @@ -1237,8 +1404,10 @@ class Namespace : public detail::Registrar ([&] { + using InnerF = detail::unwrap_fn_type_t; + detail::OverloadEntry entry; - if constexpr (detail::is_any_cfunction_pointer_v) + if constexpr (detail::is_any_cfunction_pointer_v) { entry.arity = -1; entry.checker = nullptr; @@ -1246,9 +1415,15 @@ class Namespace : public detail::Registrar else { // skip void* first arg (placement new destination, not a Lua argument) - using ArgsPack = detail::remove_first_type_t>; - entry.arity = static_cast(detail::function_arity_excluding_v) - 1; + using ArgsPack = detail::remove_first_type_t>; + entry.arity = static_cast(detail::function_arity_excluding_v) - 1; entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set->entries.push_back(entry); @@ -1261,9 +1436,10 @@ class Namespace : public detail::Registrar ([&] { - using F = detail::constructor_forwarder; + using InnerF = detail::unwrap_fn_type_t; + using F = detail::constructor_forwarder; - lua_newuserdata_aligned(L, F(std::move(functions))); + lua_newuserdata_aligned(L, F(detail::get_underlying(std::move(functions)))); lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 1); lua_rawseti(L, -2, idx++); @@ -1870,6 +2046,9 @@ class Namespace : public detail::Registrar detail::push_property_readonly(L, name); // Stack: ns, function detail::add_property_setter(L, name, -2); // Stack: ns + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); // ns + return *this; } @@ -1901,6 +2080,9 @@ class Namespace : public detail::Registrar detail::push_property_setter(L, std::move(setter), name); // Stack: ns, setter detail::add_property_setter(L, name, -2); // Stack: ns + using RetType = detail::getter_return_t; + detail::add_property_type(L, name, std::string(detail::typeName()).c_str(), -1); // ns + return *this; } @@ -1922,11 +2104,45 @@ class Namespace : public detail::Registrar if constexpr (sizeof...(Functions) == 1) { +#if LUABRIDGE_ENABLE_REFLECT + // Embed 1-entry OverloadSet as upvalue[1] of the actual function closure so that + // metadata is accessible at runtime without a wrapper (avoiding C-to-C call chains + // that break luaL_argerror function-name resolution). + ([&] + { + if constexpr (!detail::is_any_cfunction_pointer_v) + { + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + detail::OverloadEntry entry; + using ArgsPack = detail::function_arguments_t; + entry.arity = -1; // single overload: arity check disabled + entry.checker = nullptr; // single overload: no type dispatch needed + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; + overload_set->entries.push_back(std::move(entry)); + + // OverloadSet is now at stack top (upvalue[1]). + // push_function_reflect pushes function data as upvalue[2] and creates the closure. + detail::push_function_reflect(L, detail::get_underlying(std::move(functions)), name); + } + else + { + // Bare cfunction: no type metadata to embed; register directly. + detail::push_function(L, detail::get_underlying(std::move(functions)), name); + } + + } (), ...); +#else ([&] { - detail::push_function(L, std::move(functions), name); + detail::push_function(L, detail::get_underlying(std::move(functions)), name); } (), ...); +#endif } else { @@ -1947,6 +2163,12 @@ class Namespace : public detail::Registrar using ArgsPack = detail::function_arguments_t; entry.arity = static_cast(detail::function_arity_excluding_v); entry.checker = &detail::overload_type_checker; +#if LUABRIDGE_ENABLE_REFLECT + entry.returnType = std::string(detail::typeName>>()); + entry.paramTypes = detail::reflect_param_type_names(); + if constexpr (detail::is_function_with_hints_v) + entry.paramHints = functions.hints; +#endif } overload_set->entries.push_back(entry); @@ -1959,7 +2181,7 @@ class Namespace : public detail::Registrar ([&] { - detail::push_function(L, std::move(functions), name); + detail::push_function(L, detail::get_underlying(std::move(functions)), name); lua_rawseti(L, -2, idx++); } (), ...); diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 414d3a9a..3650aa56 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -33,6 +33,7 @@ set (LUABRIDGE_TEST_SOURCE_FILES Source/ClassTests.cpp Source/CoroutineTests.cpp Source/DumpTests.cpp + Source/InspectTests.cpp Source/EnumTests.cpp Source/ExceptionTests.cpp Source/FlagSetTests.cpp @@ -70,7 +71,14 @@ source_group ("Source Files" FILES ${LUABRIDGE_TEST_SOURCE_FILES}) # ====================================================== Lua C -set (LUABRIDGE_LUA_C_DEFINES "LUABRIDGE_SAFE_LUA_C_EXCEPTION_HANDLING=1") +set (LUABRIDGE_LUA_DEFAULT_DEFINES) + +set (LUABRIDGE_LUA_CPP_DEFINES + ${LUABRIDGE_LUA_DEFAULT_DEFINES}) + +set (LUABRIDGE_LUA_C_DEFINES + ${LUABRIDGE_LUA_DEFAULT_DEFINES} + -DLUABRIDGE_SAFE_LUA_C_EXCEPTION_HANDLING=1) # ====================================================== Lua 5.1 @@ -198,63 +206,45 @@ function (setup_target_for_coverage TARGET_NAME SOURCE_LOCATION SOURCE_PACKAGE) endfunction () function (setup_coverage_single_target) + set (LUABRIDGE_COVERAGE_TARGETS) + + foreach (LUA_VERSION_SUFFIX IN ITEMS 51 52 53 54 55) + foreach (TEST_VARIANT IN ITEMS "" "Noexcept" "Reflect" "ReflectNoexcept" "LuaC" "LuaCNoexcept") + list (APPEND LUABRIDGE_COVERAGE_TARGETS "LuaBridgeTests${LUA_VERSION_SUFFIX}${TEST_VARIANT}") + endforeach () + endforeach () + + list (APPEND LUABRIDGE_COVERAGE_TARGETS + LuaBridgeTestsLuau + LuaBridgeTestsLuauReflect) + + if (NOT EMSCRIPTEN) + list (APPEND LUABRIDGE_COVERAGE_TARGETS + LuaBridgeTestsLuaJIT + LuaBridgeTestsLuaJITNoexcept + LuaBridgeTestsLuaJITReflect + LuaBridgeTestsLuaJITReflectNoexcept + LuaBridgeTestsRavi + LuaBridgeTestsRaviReflect) + endif () + + set (LUABRIDGE_COVERAGE_INFO_FILES) + set (LUABRIDGE_COVERAGE_MERGE_ARGS) + foreach (COVERAGE_TARGET IN LISTS LUABRIDGE_COVERAGE_TARGETS) + list (APPEND LUABRIDGE_COVERAGE_INFO_FILES "coverage/${COVERAGE_TARGET}.info") + list (APPEND LUABRIDGE_COVERAGE_MERGE_ARGS -a "coverage/${COVERAGE_TARGET}.info") + endforeach () + add_custom_target (LuaBridgeTestsCoverage COMMAND ${CMAKE_COMMAND} -E make_directory coverage_html COMMAND ${CMAKE_COMMAND} -E rm -Rf coverage_html/* COMMAND ${CMAKE_COMMAND} -E rm -f coverage/Merged.info COMMAND ${LCOV_EXECUTABLE} - -a "coverage/LuaBridgeTests51.info" - -a "coverage/LuaBridgeTests51LuaC.info" - -a "coverage/LuaBridgeTests51Noexcept.info" - -a "coverage/LuaBridgeTests51LuaCNoexcept.info" - -a "coverage/LuaBridgeTests52.info" - -a "coverage/LuaBridgeTests52LuaC.info" - -a "coverage/LuaBridgeTests52Noexcept.info" - -a "coverage/LuaBridgeTests52LuaCNoexcept.info" - -a "coverage/LuaBridgeTests53.info" - -a "coverage/LuaBridgeTests53LuaC.info" - -a "coverage/LuaBridgeTests53Noexcept.info" - -a "coverage/LuaBridgeTests53LuaCNoexcept.info" - -a "coverage/LuaBridgeTests54.info" - -a "coverage/LuaBridgeTests54LuaC.info" - -a "coverage/LuaBridgeTests54Noexcept.info" - -a "coverage/LuaBridgeTests54LuaCNoexcept.info" - -a "coverage/LuaBridgeTests55.info" - -a "coverage/LuaBridgeTests55LuaC.info" - -a "coverage/LuaBridgeTests55Noexcept.info" - -a "coverage/LuaBridgeTests55LuaCNoexcept.info" - -a "coverage/LuaBridgeTestsLuaJIT.info" - -a "coverage/LuaBridgeTestsLuaJITNoexcept.info" - -a "coverage/LuaBridgeTestsLuau.info" - -a "coverage/LuaBridgeTestsRavi.info" + ${LUABRIDGE_COVERAGE_MERGE_ARGS} -o "coverage/Merged.info" COMMAND ${GENHTML_EXECUTABLE} --rc branch_coverage=1 "coverage/Merged.info" -o "coverage_html" - DEPENDS - "coverage/LuaBridgeTests51.info" - "coverage/LuaBridgeTests51LuaC.info" - "coverage/LuaBridgeTests51Noexcept.info" - "coverage/LuaBridgeTests51LuaCNoexcept.info" - "coverage/LuaBridgeTests52.info" - "coverage/LuaBridgeTests52LuaC.info" - "coverage/LuaBridgeTests52Noexcept.info" - "coverage/LuaBridgeTests52LuaCNoexcept.info" - "coverage/LuaBridgeTests53.info" - "coverage/LuaBridgeTests53LuaC.info" - "coverage/LuaBridgeTests53Noexcept.info" - "coverage/LuaBridgeTests53LuaCNoexcept.info" - "coverage/LuaBridgeTests54.info" - "coverage/LuaBridgeTests54LuaC.info" - "coverage/LuaBridgeTests54Noexcept.info" - "coverage/LuaBridgeTests54LuaCNoexcept.info" - "coverage/LuaBridgeTests55.info" - "coverage/LuaBridgeTests55LuaC.info" - "coverage/LuaBridgeTests55Noexcept.info" - "coverage/LuaBridgeTests55LuaCNoexcept.info" - "coverage/LuaBridgeTestsLuaJIT.info" - "coverage/LuaBridgeTestsLuaJITNoexcept.info" - "coverage/LuaBridgeTestsLuau.info" - "coverage/LuaBridgeTestsRavi.info" + DEPENDS ${LUABRIDGE_COVERAGE_INFO_FILES} WORKING_DIRECTORY ${CMAKE_BINARY_DIR} VERBATIM) @@ -348,16 +338,18 @@ macro (add_test_app LUABRIDGE_TEST_NAME LUA_VERSION LUABRIDGE_TEST_LUA_LIBRARY_F Source) if (${LUA_VERSION} STREQUAL "LUAU") - target_include_directories (${LUABRIDGE_TEST_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/VM/include") - target_include_directories (${LUABRIDGE_TEST_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/Ast/include") - target_include_directories (${LUABRIDGE_TEST_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/Compiler/include") - target_include_directories (${LUABRIDGE_TEST_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/Common/include") + target_include_directories (${LUABRIDGE_TEST_NAME} PRIVATE + "${LUABRIDGE_LUAU_LOCATION}/VM/include" + "${LUABRIDGE_LUAU_LOCATION}/Ast/include" + "${LUABRIDGE_LUAU_LOCATION}/Compiler/include" + "${LUABRIDGE_LUAU_LOCATION}/Common/include") target_compile_definitions (${LUABRIDGE_TEST_NAME} PRIVATE LUABRIDGE_TEST_LUAU=1) if (LUABRIDGE_BUILD_SHARED_TESTS) - target_include_directories (${LUABRIDGE_TESTLIB_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/VM/include") - target_include_directories (${LUABRIDGE_TESTLIB_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/Ast/include") - target_include_directories (${LUABRIDGE_TESTLIB_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/Compiler/include") - target_include_directories (${LUABRIDGE_TESTLIB_NAME} PRIVATE "${LUABRIDGE_LUAU_LOCATION}/Common/include") + target_include_directories (${LUABRIDGE_TESTLIB_NAME} PRIVATE + "${LUABRIDGE_LUAU_LOCATION}/VM/include" + "${LUABRIDGE_LUAU_LOCATION}/Ast/include" + "${LUABRIDGE_LUAU_LOCATION}/Compiler/include" + "${LUABRIDGE_LUAU_LOCATION}/Common/include") target_compile_definitions (${LUABRIDGE_TESTLIB_NAME} PRIVATE LUABRIDGE_TEST_LUAU=1) endif () elseif (${LUA_VERSION} STREQUAL "LUAJIT") @@ -387,7 +379,8 @@ macro (add_test_app LUABRIDGE_TEST_NAME LUA_VERSION LUABRIDGE_TEST_LUA_LIBRARY_F endif () target_compile_definitions (${LUABRIDGE_TEST_NAME} PRIVATE LUABRIDGE_TEST_SHARED_LIBRARY="${LUABRIDGE_TEST_SHARED_LIBRARY}" - LUABRIDGE_TEST_SHARED_EXPORT=0) + LUABRIDGE_TEST_SHARED_EXPORT=0 + ${LUABRIDGE_DEFINES}) endif () if (NOT ${LUABRIDGE_EXCEPTIONS}) @@ -433,43 +426,57 @@ endmacro (add_test_app) # ====================================================== Real Unit Tests -add_test_app (LuaBridgeTests51 501 "${LUABRIDGE_TEST_LUA51_FILES}" 1 "" "") +add_test_app (LuaBridgeTests51 501 "${LUABRIDGE_TEST_LUA51_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests51Noexcept 501 "${LUABRIDGE_TEST_LUA51_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests51Reflect 501 "${LUABRIDGE_TEST_LUA51_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests51ReflectNoexcept 501 "${LUABRIDGE_TEST_LUA51_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") add_test_app (LuaBridgeTests51LuaC 501 "${LUABRIDGE_TEST_LUA51_C_FILES}" 1 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests51Noexcept 501 "${LUABRIDGE_TEST_LUA51_FILES}" 0 "" "") add_test_app (LuaBridgeTests51LuaCNoexcept 501 "${LUABRIDGE_TEST_LUA51_C_FILES}" 0 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests52 502 "${LUABRIDGE_TEST_LUA52_FILES}" 1 "" "") +add_test_app (LuaBridgeTests52 502 "${LUABRIDGE_TEST_LUA52_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests52Noexcept 502 "${LUABRIDGE_TEST_LUA52_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests52Reflect 502 "${LUABRIDGE_TEST_LUA52_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests52ReflectNoexcept 502 "${LUABRIDGE_TEST_LUA52_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") add_test_app (LuaBridgeTests52LuaC 502 "${LUABRIDGE_TEST_LUA52_C_FILES}" 1 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests52Noexcept 502 "${LUABRIDGE_TEST_LUA52_FILES}" 0 "" "") add_test_app (LuaBridgeTests52LuaCNoexcept 502 "${LUABRIDGE_TEST_LUA52_C_FILES}" 0 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests53 503 "${LUABRIDGE_TEST_LUA53_FILES}" 1 "" "") +add_test_app (LuaBridgeTests53 503 "${LUABRIDGE_TEST_LUA53_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests53Noexcept 503 "${LUABRIDGE_TEST_LUA53_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests53Reflect 503 "${LUABRIDGE_TEST_LUA53_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests53ReflectNoexcept 503 "${LUABRIDGE_TEST_LUA53_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") add_test_app (LuaBridgeTests53LuaC 503 "${LUABRIDGE_TEST_LUA53_C_FILES}" 1 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests53Noexcept 503 "${LUABRIDGE_TEST_LUA53_FILES}" 0 "" "") add_test_app (LuaBridgeTests53LuaCNoexcept 503 "${LUABRIDGE_TEST_LUA53_C_FILES}" 0 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests54 504 "${LUABRIDGE_TEST_LUA54_FILES}" 1 "" "") +add_test_app (LuaBridgeTests54 504 "${LUABRIDGE_TEST_LUA54_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests54Noexcept 504 "${LUABRIDGE_TEST_LUA54_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests54Reflect 504 "${LUABRIDGE_TEST_LUA54_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests54ReflectNoexcept 504 "${LUABRIDGE_TEST_LUA54_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") add_test_app (LuaBridgeTests54LuaC 504 "${LUABRIDGE_TEST_LUA54_C_FILES}" 1 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests54Noexcept 504 "${LUABRIDGE_TEST_LUA54_FILES}" 0 "" "") add_test_app (LuaBridgeTests54LuaCNoexcept 504 "${LUABRIDGE_TEST_LUA54_C_FILES}" 0 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests55 505 "${LUABRIDGE_TEST_LUA55_FILES}" 1 "" "") +add_test_app (LuaBridgeTests55 505 "${LUABRIDGE_TEST_LUA55_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests55Noexcept 505 "${LUABRIDGE_TEST_LUA55_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests55Reflect 505 "${LUABRIDGE_TEST_LUA55_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTests55ReflectNoexcept 505 "${LUABRIDGE_TEST_LUA55_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") add_test_app (LuaBridgeTests55LuaC 505 "${LUABRIDGE_TEST_LUA55_C_FILES}" 1 "${LUABRIDGE_LUA_C_DEFINES}" "") -add_test_app (LuaBridgeTests55Noexcept 505 "${LUABRIDGE_TEST_LUA55_FILES}" 0 "" "") add_test_app (LuaBridgeTests55LuaCNoexcept 505 "${LUABRIDGE_TEST_LUA55_C_FILES}" 0 "${LUABRIDGE_LUA_C_DEFINES}" "") -if (NOT EMSCRIPTEN) - add_test_app (LuaBridgeTestsLuaJIT "LUAJIT" "${LUABRIDGE_TEST_LUAJIT_FILES}" 1 "" "liblua-static") - add_test_app (LuaBridgeTestsLuaJITNoexcept "LUAJIT" "${LUABRIDGE_TEST_LUAJIT_FILES}" 0 "" "liblua-static") -endif () - -add_test_app (LuaBridgeTestsLuau "LUAU" "${LUABRIDGE_TEST_LUAU_FILES}" 1 "" "") -#add_test_app (LuaBridgeTestsLuauNoexcept "LUAU" "${LUABRIDGE_TEST_LUAU_FILES}" 0 "" "") +add_test_app (LuaBridgeTestsLuau "LUAU" "${LUABRIDGE_TEST_LUAU_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +#add_test_app (LuaBridgeTestsLuauNoexcept "LUAU" "${LUABRIDGE_TEST_LUAU_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "") +add_test_app (LuaBridgeTestsLuauReflect "LUAU" "${LUABRIDGE_TEST_LUAU_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") +#add_test_app (LuaBridgeTestsLuauReflectNoexcept "LUAU" "${LUABRIDGE_TEST_LUAU_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "") if (NOT EMSCRIPTEN) + add_test_app (LuaBridgeTestsLuaJIT "LUAJIT" "${LUABRIDGE_TEST_LUAJIT_FILES}" 1 "${LUABRIDGE_LUA_CPP_DEFINES}" "liblua-static") + add_test_app (LuaBridgeTestsLuaJITNoexcept "LUAJIT" "${LUABRIDGE_TEST_LUAJIT_FILES}" 0 "${LUABRIDGE_LUA_CPP_DEFINES}" "liblua-static") + add_test_app (LuaBridgeTestsLuaJITReflect "LUAJIT" "${LUABRIDGE_TEST_LUAJIT_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "liblua-static") + add_test_app (LuaBridgeTestsLuaJITReflectNoexcept "LUAJIT" "${LUABRIDGE_TEST_LUAJIT_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_CPP_DEFINES}" "liblua-static") + add_test_app (LuaBridgeTestsRavi "RAVI" "${LUABRIDGE_TEST_RAVI_FILES}" 1 "${LUABRIDGE_LUA_C_DEFINES}" "libravi") + #add_test_app (LuaBridgeTestsRaviNoexcept "RAVI" "${LUABRIDGE_TEST_RAVI_FILES}" 0 "${LUABRIDGE_LUA_C_DEFINES}" "libravi") + add_test_app (LuaBridgeTestsRaviReflect "RAVI" "${LUABRIDGE_TEST_RAVI_FILES}" 1 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_C_DEFINES}" "libravi") + #add_test_app (LuaBridgeTestsRaviReflectNoexcept "RAVI" "${LUABRIDGE_TEST_RAVI_FILES}" 0 "-DLUABRIDGE_ENABLE_REFLECT=1;${LUABRIDGE_LUA_C_DEFINES}" "libravi") endif () -#add_test_app (LuaBridgeTestsRaviNoexcept "RAVI" "${LUABRIDGE_TEST_RAVI_FILES}" 0 "" "libravi") if (LUABRIDGE_COVERAGE) setup_coverage_single_target () diff --git a/Tests/Lua/Luau.cpp b/Tests/Lua/Luau.cpp index 297cde0e..bc5f5580 100644 --- a/Tests/Lua/Luau.cpp +++ b/Tests/Lua/Luau.cpp @@ -36,6 +36,10 @@ #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif +#elif __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wunused-variable" #endif // Ast @@ -83,6 +87,8 @@ #if _MSC_VER #pragma pop_macro("_CRT_SECURE_NO_WARNINGS") +#elif __GNUC__ +#pragma GCC diagnostic pop #endif #else diff --git a/Tests/Lua/LuauSplit.cpp b/Tests/Lua/LuauSplit.cpp index 4fec0a70..6f7155a8 100644 --- a/Tests/Lua/LuauSplit.cpp +++ b/Tests/Lua/LuauSplit.cpp @@ -36,6 +36,10 @@ #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif +#elif __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" +#pragma GCC diagnostic ignored "-Wunused-variable" #endif // Compiler @@ -63,6 +67,8 @@ #if _MSC_VER #pragma pop_macro("_CRT_SECURE_NO_WARNINGS") +#elif __GNUC__ +#pragma GCC diagnostic pop #endif #else diff --git a/Tests/Source/InspectTests.cpp b/Tests/Source/InspectTests.cpp new file mode 100644 index 00000000..2dc469ef --- /dev/null +++ b/Tests/Source/InspectTests.cpp @@ -0,0 +1,529 @@ +// https://github.com/kunitoki/LuaBridge3 +// Copyright 2026, kunitoki +// SPDX-License-Identifier: MIT + +#include "TestBase.h" + +#include "LuaBridge/LuaBridge.h" + +#if LUABRIDGE_ENABLE_REFLECT +#include "LuaBridge/Inspect.h" + +#include +#include + +struct InspectTests : TestBase +{ +}; + +//================================================================================================= +// Helpers + +namespace { + +const luabridge::MemberInfo* findMember(const luabridge::ClassInspectInfo& cls, const std::string& name) +{ + for (const auto& m : cls.members) + if (m.name == name) return &m; + return nullptr; +} + +const luabridge::MemberInfo* findFreeMember(const luabridge::NamespaceInspectInfo& ns, const std::string& name) +{ + for (const auto& m : ns.freeMembers) + if (m.name == name) return &m; + return nullptr; +} + +const luabridge::ClassInspectInfo* findClass(const luabridge::NamespaceInspectInfo& ns, const std::string& name) +{ + for (const auto& c : ns.classes) + if (c.name == name) return &c; + return nullptr; +} + +const luabridge::NamespaceInspectInfo* findSubNamespace(const luabridge::NamespaceInspectInfo& ns, const std::string& name) +{ + for (const auto& s : ns.subNamespaces) + if (s.name == name) return &s; + return nullptr; +} + +bool hasBase(const luabridge::ClassInspectInfo& cls, const std::string& name) +{ + return std::find(cls.baseClasses.begin(), cls.baseClasses.end(), name) != cls.baseClasses.end(); +} + +} // anonymous namespace + +//================================================================================================= +// inspect() — single class tests + +TEST_F(InspectTests, InspectUnregisteredClass) +{ + struct Unregistered {}; + auto info = luabridge::inspect(L); + EXPECT_TRUE(info.name.empty()); + EXPECT_TRUE(info.members.empty()); + EXPECT_TRUE(info.baseClasses.empty()); +} + +TEST_F(InspectTests, InspectSimpleClass) +{ + struct Foo + { + int x = 0; + int getX() const { return x; } + void setX(int v) { x = v; } + void doSomething() {} + static int staticFunc() { return 42; } + }; + + luabridge::getGlobalNamespace(L) + .beginClass("Foo") + .addConstructor() + .addProperty("x", &Foo::getX, &Foo::setX) + .addFunction("doSomething", &Foo::doSomething) + .addStaticFunction("staticFunc", &Foo::staticFunc) + .endClass(); + + auto info = luabridge::inspect(L); + EXPECT_EQ("Foo", info.name); + + auto* prop = findMember(info, "x"); + ASSERT_NE(nullptr, prop); + EXPECT_EQ(luabridge::MemberKind::Property, prop->kind); + + auto* method = findMember(info, "doSomething"); + ASSERT_NE(nullptr, method); + EXPECT_EQ(luabridge::MemberKind::Method, method->kind); + EXPECT_EQ(1u, method->overloads.size()); + + auto* staticFn = findMember(info, "staticFunc"); + ASSERT_NE(nullptr, staticFn); + EXPECT_EQ(luabridge::MemberKind::StaticMethod, staticFn->kind); + + auto* ctor = findMember(info, "__call"); + ASSERT_NE(nullptr, ctor); + EXPECT_EQ(luabridge::MemberKind::Constructor, ctor->kind); +} + +TEST_F(InspectTests, InspectReadOnlyProperty) +{ + struct Bar { int getValue() const { return 1; } }; + luabridge::getGlobalNamespace(L) + .beginClass("Bar") + .addProperty("value", &Bar::getValue) + .endClass(); + + auto info = luabridge::inspect(L); + auto* prop = findMember(info, "value"); + ASSERT_NE(nullptr, prop); + EXPECT_EQ(luabridge::MemberKind::ReadOnlyProperty, prop->kind); +} + +TEST_F(InspectTests, InspectOverloadedMethod) +{ + struct Baz {}; + luabridge::getGlobalNamespace(L) + .beginClass("Baz") + .addFunction("method", + [](Baz&, int) {}, + [](Baz&, float) {}) + .endClass(); + + auto info = luabridge::inspect(L); + auto* method = findMember(info, "method"); + ASSERT_NE(nullptr, method); + EXPECT_EQ(luabridge::MemberKind::Method, method->kind); + EXPECT_EQ(2u, method->overloads.size()); +} + +TEST_F(InspectTests, InspectDerivedClass) +{ + struct Base { int baseMethod() const { return 1; } }; + struct Derived : Base { int derivedMethod() const { return 2; } }; + + luabridge::getGlobalNamespace(L) + .beginClass("Base") + .addFunction("baseMethod", &Base::baseMethod) + .endClass() + .deriveClass("Derived") + .addFunction("derivedMethod", &Derived::derivedMethod) + .endClass(); + + auto info = luabridge::inspect(L); + EXPECT_EQ("Derived", info.name); + EXPECT_FALSE(info.baseClasses.empty()); + EXPECT_TRUE(hasBase(info, "Base")); +} + +TEST_F(InspectTests, InspectMetamethods) +{ + struct Vec + { + Vec operator+(const Vec& /*rhs*/) const { return {}; } + }; + luabridge::getGlobalNamespace(L) + .beginClass("Vec") + .addFunction("__add", &Vec::operator+) + .endClass(); + + auto info = luabridge::inspect(L); + auto* add = findMember(info, "__add"); + ASSERT_NE(nullptr, add); + EXPECT_EQ(luabridge::MemberKind::Metamethod, add->kind); +} + +TEST_F(InspectTests, InspectStaticReadOnlyProperty) +{ + struct WithStatic {}; + static int sVal = 42; + luabridge::getGlobalNamespace(L) + .beginClass("WithStatic") + .addStaticProperty("ROProp", +[]() { return sVal; }) + .addStaticProperty("RWProp", +[]() { return sVal; }, +[](int v) { sVal = v; }) + .endClass(); + + auto info = luabridge::inspect(L); + + auto* ro = findMember(info, "ROProp"); + ASSERT_NE(nullptr, ro); + EXPECT_EQ(luabridge::MemberKind::StaticReadOnlyProperty, ro->kind); + + auto* rw = findMember(info, "RWProp"); + ASSERT_NE(nullptr, rw); + EXPECT_EQ(luabridge::MemberKind::StaticProperty, rw->kind); +} + +//================================================================================================= +// inspectNamespace() tests + +TEST_F(InspectTests, InspectNamespace) +{ + struct Widget { void draw() {} }; + luabridge::getGlobalNamespace(L) + .beginNamespace("UI") + .beginClass("Widget") + .addFunction("draw", &Widget::draw) + .endClass() + .addFunction("ping", +[]() -> int { return 1; }) + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "UI"); + EXPECT_EQ("UI", ns.name); + + auto* w = findClass(ns, "Widget"); + ASSERT_NE(nullptr, w); + EXPECT_EQ("Widget", w->name); + + auto* ping = findFreeMember(ns, "ping"); + ASSERT_NE(nullptr, ping); + EXPECT_EQ(luabridge::MemberKind::Method, ping->kind); +} + +TEST_F(InspectTests, InspectSubNamespace) +{ + luabridge::getGlobalNamespace(L) + .beginNamespace("Outer") + .beginNamespace("Inner") + .addFunction("innerFunc", +[]() {}) + .endNamespace() + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "Outer"); + EXPECT_EQ("Outer", ns.name); + + auto* inner = findSubNamespace(ns, "Inner"); + ASSERT_NE(nullptr, inner); + EXPECT_EQ("Inner", inner->name); + + auto* fn = findFreeMember(*inner, "innerFunc"); + ASSERT_NE(nullptr, fn); +} + +TEST_F(InspectTests, InspectEmptyNamespace) +{ + luabridge::getGlobalNamespace(L) + .beginNamespace("Empty") + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "Empty"); + EXPECT_EQ("Empty", ns.name); + EXPECT_TRUE(ns.classes.empty()); + EXPECT_TRUE(ns.freeMembers.empty()); + EXPECT_TRUE(ns.subNamespaces.empty()); +} + +TEST_F(InspectTests, InspectNonexistentNamespace) +{ + auto ns = luabridge::inspectNamespace(L, "DoesNotExist"); + EXPECT_TRUE(ns.name.empty()); +} + +TEST_F(InspectTests, InspectDottedNamespacePath) +{ + luabridge::getGlobalNamespace(L) + .beginNamespace("A") + .beginNamespace("B") + .addFunction("deep", +[]() {}) + .endNamespace() + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "A.B"); + EXPECT_EQ("B", ns.name); + EXPECT_NE(nullptr, findFreeMember(ns, "deep")); +} + +TEST_F(InspectTests, InspectNamespaceProperty) +{ + static int nsVal = 0; + luabridge::getGlobalNamespace(L) + .beginNamespace("NsProp") + .addProperty("roProp", +[]() { return nsVal; }) + .addProperty("rwProp", +[]() { return nsVal; }, +[](int v) { nsVal = v; }) + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "NsProp"); + auto* ro = findFreeMember(ns, "roProp"); + ASSERT_NE(nullptr, ro); + EXPECT_EQ(luabridge::MemberKind::ReadOnlyProperty, ro->kind); + + auto* rw = findFreeMember(ns, "rwProp"); + ASSERT_NE(nullptr, rw); + EXPECT_EQ(luabridge::MemberKind::Property, rw->kind); +} + +//================================================================================================= +// Visitor tests + +TEST_F(InspectTests, ConsoleVisitorProducesOutput) +{ + struct Cls {}; + luabridge::getGlobalNamespace(L) + .beginNamespace("ConsTest") + .beginClass("Cls") + .addConstructor() + .endClass() + .endNamespace(); + + std::stringstream ss; + luabridge::inspectPrint(L, "ConsTest", ss); + EXPECT_FALSE(ss.str().empty()); + EXPECT_NE(std::string::npos, ss.str().find("ConsTest")); + EXPECT_NE(std::string::npos, ss.str().find("Cls")); +} + +TEST_F(InspectTests, LuaLSVisitorProducesAnnotations) +{ + struct Cls { int getValue() const { return 0; } }; + luabridge::getGlobalNamespace(L) + .beginNamespace("LuaLS") + .beginClass("Cls") + .addConstructor() + .addProperty("value", &Cls::getValue) + .addFunction("xyz", [](const Cls& self, int a) { return a + 1; }) + .addStaticFunction("abc", [](bool, float, int) { return "42"; }) + .endClass() + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "LuaLS"); + std::stringstream ss; + luabridge::LuaLSVisitor v(ss); + luabridge::accept(ns, v); + + const auto result = ss.str(); + std::cout << result << std::endl; + EXPECT_FALSE(result.empty()); + EXPECT_NE(std::string::npos, result.find("---@class LuaLS.Cls")); + EXPECT_NE(std::string::npos, result.find("---@field value integer # readonly")); + EXPECT_NE(std::string::npos, result.find("---@overload fun(): LuaLS.Cls")); + EXPECT_NE(std::string::npos, result.find("local Cls = {}")); + EXPECT_NE(std::string::npos, result.find("---@param self LuaLS.Cls")); + EXPECT_NE(std::string::npos, result.find("---@param p1 integer")); + EXPECT_NE(std::string::npos, result.find("---@return integer")); + EXPECT_NE(std::string::npos, result.find("function LuaLS.Cls:xyz(p1) end")); + EXPECT_NE(std::string::npos, result.find("---@param p1 boolean")); + EXPECT_NE(std::string::npos, result.find("---@param p2 number")); + EXPECT_NE(std::string::npos, result.find("---@param p3 integer")); + EXPECT_NE(std::string::npos, result.find("---@return string")); + EXPECT_NE(std::string::npos, result.find("function LuaLS.Cls.abc(p1, p2, p3) end")); +} + +TEST_F(InspectTests, LuaProxyVisitorProducesStubs) +{ + struct Cls { void method() {} }; + luabridge::getGlobalNamespace(L) + .beginNamespace("Proxy") + .beginClass("Cls") + .addConstructor() + .addFunction("method", &Cls::method) + .endClass() + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "Proxy"); + std::stringstream ss; + luabridge::LuaProxyVisitor v(ss); + luabridge::accept(ns, v); + + const auto result = ss.str(); + EXPECT_FALSE(result.empty()); + EXPECT_NE(std::string::npos, result.find("Cls")); + EXPECT_NE(std::string::npos, result.find("method")); +} + +//================================================================================================= +// inspect() stack balance test + +TEST_F(InspectTests, InspectDoesNotLeakStack) +{ + struct Foo { void m() {} }; + luabridge::getGlobalNamespace(L) + .beginClass("Foo") + .addFunction("m", &Foo::m) + .endClass(); + + int top = lua_gettop(L); + auto info = luabridge::inspect(L); + EXPECT_EQ(top, lua_gettop(L)); + EXPECT_EQ("Foo", info.name); +} + +TEST_F(InspectTests, InspectNamespaceDoesNotLeakStack) +{ + luabridge::getGlobalNamespace(L) + .beginNamespace("StackTest") + .addFunction("f", +[]() {}) + .endNamespace(); + + int top = lua_gettop(L); + auto ns = luabridge::inspectNamespace(L, "StackTest"); + EXPECT_EQ(top, lua_gettop(L)); + EXPECT_EQ("StackTest", ns.name); +} + +//================================================================================================= +// inspectAccept() helper + +TEST_F(InspectTests, InspectAcceptDrivesVisitor) +{ + struct Cls { void m() {} }; + luabridge::getGlobalNamespace(L) + .beginNamespace("AccTest") + .beginClass("Cls") + .addFunction("m", &Cls::m) + .endClass() + .endNamespace(); + + struct CountingVisitor : luabridge::InspectVisitor + { + int classCount = 0; + int memberCount = 0; + void beginClass(const luabridge::ClassInspectInfo&) override { ++classCount; } + void visitMember(const luabridge::ClassInspectInfo&, const luabridge::MemberInfo&) override { ++memberCount; } + }; + + CountingVisitor cv; + luabridge::inspectAccept(L, "AccTest", cv); + EXPECT_EQ(1, cv.classCount); + EXPECT_GE(cv.memberCount, 1); +} + +//================================================================================================= +// withHints() API test (tests FunctionWithHints detection) + +TEST_F(InspectTests, WithHintsIsAcceptedByAddFunction) +{ + struct Actor { void attack(float /*damage*/) {} }; + + // This test verifies that withHints compiles and the function is registered correctly. + luabridge::getGlobalNamespace(L) + .beginClass("Actor") + .addFunction("attack", luabridge::withHints(&Actor::attack, "damage")) + .endClass(); + + // Verify the function is callable from Lua + auto info = luabridge::inspect(L); + auto* m = findMember(info, "attack"); + ASSERT_NE(nullptr, m); + EXPECT_EQ(luabridge::MemberKind::Method, m->kind); +} + +TEST_F(InspectTests, WithHintsOverloadsCompile) +{ + struct Shooter {}; + + luabridge::getGlobalNamespace(L) + .beginClass("Shooter") + .addFunction("fire", + luabridge::withHints([](Shooter&, int /*count*/) {}, "count"), + luabridge::withHints([](Shooter&, float /*angle*/, int /*count*/) {}, "angle", "count")) + .endClass(); + + auto info = luabridge::inspect(L); + auto* m = findMember(info, "fire"); + ASSERT_NE(nullptr, m); + EXPECT_EQ(2u, m->overloads.size()); +} + +TEST_F(InspectTests, ReflectTypeInfoPopulated) +{ + struct Enemy { void takeDamage(float /*amount*/, int /*count*/) {} }; + + luabridge::getGlobalNamespace(L) + .beginClass("Enemy") + .addFunction("takeDamage", luabridge::withHints(&Enemy::takeDamage, "amount", "count")) + .endClass(); + + auto info = luabridge::inspect(L); + auto* m = findMember(info, "takeDamage"); + ASSERT_NE(nullptr, m); + ASSERT_EQ(1u, m->overloads.size()); + + const auto& ov = m->overloads[0]; + ASSERT_EQ(2u, ov.params.size()); + + // Type names should be non-empty when LUABRIDGE_ENABLE_REFLECT is active + EXPECT_FALSE(ov.params[0].typeName.empty()); + EXPECT_FALSE(ov.params[1].typeName.empty()); + + // Hints should be populated from withHints + EXPECT_EQ("amount", ov.params[0].hint); + EXPECT_EQ("count", ov.params[1].hint); +} + +TEST_F(InspectTests, ReflectConstructorTypeInfo) +{ + struct Widget { int x; float y; }; + + luabridge::getGlobalNamespace(L) + .beginClass("Widget") + .addConstructor() + .endClass(); + + auto info = luabridge::inspect(L); + auto* ctor = findMember(info, "__call"); + ASSERT_NE(nullptr, ctor); + ASSERT_EQ(1u, ctor->overloads.size()); + + EXPECT_EQ(2u, ctor->overloads[0].params.size()); +} + +TEST_F(InspectTests, ReflectFreeFunction) +{ + luabridge::getGlobalNamespace(L) + .beginNamespace("ReflectNS") + .addFunction("add", + luabridge::withHints(+[](int a, int b) -> int { return a + b; }, "a", "b")) + .endNamespace(); + + auto ns = luabridge::inspectNamespace(L, "ReflectNS"); + auto* fn = findFreeMember(ns, "add"); + ASSERT_NE(nullptr, fn); + ASSERT_EQ(1u, fn->overloads.size()); + ASSERT_EQ(2u, fn->overloads[0].params.size()); + EXPECT_EQ("a", fn->overloads[0].params[0].hint); + EXPECT_EQ("b", fn->overloads[0].params[1].hint); +} + +#endif // LUABRIDGE_ENABLE_REFLECT diff --git a/amalgamate.py b/Tools/amalgamate.py similarity index 94% rename from amalgamate.py rename to Tools/amalgamate.py index e12f99b5..1a23a815 100644 --- a/amalgamate.py +++ b/Tools/amalgamate.py @@ -196,8 +196,14 @@ def WriteAlgamationFiles(self): headerAmalgamation.write(f"// clang-format off\n\n") headerAmalgamation.write(f"#pragma once\n\n") + guarded_includes = set(["coroutine", "span"]) for header in sorted(list(self.systemHeaders)): - headerAmalgamation.write(f"#include <{header}>\n") + if header in guarded_includes: + headerAmalgamation.write(f"#if defined(__has_include) && __has_include(<{header}>)\n") + headerAmalgamation.write(f"#include <{header}>\n") + headerAmalgamation.write(f"#endif\n\n") + else: + headerAmalgamation.write(f"#include <{header}>\n") headerAmalgamation.write("\n") self.AmalgamateQueue(self.headerQueue, headerAmalgamation) diff --git a/cobertura.py b/Tools/cobertura.py similarity index 100% rename from cobertura.py rename to Tools/cobertura.py diff --git a/justfile b/justfile index 9a84bf48..67f15fae 100644 --- a/justfile +++ b/justfile @@ -22,8 +22,8 @@ plot: uv run --with-requirements Benchmarks/requirements.txt Benchmarks/plot_benchmarks.py --input Build/*.json --output Images/benchmarks.png clean: - rm -rf Build + -rm -rf Build/ + rm -rf Build/ amalgamate: - uv run amalgamate.py - + uv run Tools/amalgamate.py