Game Development 23 min read

Implementing a Lua Bridge with C++ Reflection – Lura Library Overview

The article shows how the Lura library uses C++ reflection to build a concise Lua bridge that automatically registers functions, properties, and object lifecycles via userdata and meta‑tables, simplifying bridge code compared to traditional libraries while supporting coroutines and profiling.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
Implementing a Lua Bridge with C++ Reflection – Lura Library Overview

This article uses the Lura library as an example to demonstrate how C++ reflection can serve as infrastructure for building a more concise Lua bridge. It assumes some familiarity with the Lua C API and Lua metatables.

In the previous article "C++ Reflection: Deep Dive into Function Implementation" we introduced the Function part of the reflection system. This article focuses on the Lura part.

1. Core Functions of a Lua Bridge

The bridge’s main responsibilities are to export C++ classes to Lua, which includes:

Exporting member functions and static functions

Exporting class properties (member variables)

Handling C++ object to userdata conversion

(1) Function handling

Both member and static functions are treated uniformly. The goal is to register a C++ function as a Lua C function with a unified signature. Most modern bridge libraries (luabind, luatinker, luabridge) achieve this with C++ templates, while tolua++ generates wrapper code.

Example reference: C++ Reflection: Deep Dive into Function Implementation!

(2) Property handling

Properties rely on custom __index/__newindex metamethods that access the underlying C++ object's members via userdata.

(3) C++ object → userdata

Objects are wrapped in userdata and associated with a metatable that provides __index, __newindex, __gc, etc. Lura uses a UserObject wrapper to hold the reflected object.

2. History of Lura

Previously used Lua bridges include:

luabind – Boost‑based, feature‑rich

luatinker – Simplified, no Boost dependency

tolua++ – Code‑generator approach used by Cocos2dx

luabridge – Used in a prior project, works well with a libclang exporter

Other notable bridges (e.g., sol2) are omitted for brevity.

3. Practical Summary of Existing Bridges

Feature‑complete, covering core bridge functions

Easy to maintain when paired with an exporter

Most implementations (except luabind) have concise core code

C++/Lua boundary is clear, facilitating debugging and profiling

Complex features (e.g., overriding virtual functions) are feasible

Common drawbacks include object uniqueness, type loss, and lifecycle management.

(4) Re‑thinking with Ponder

Ponder demonstrates exposing reflection info to Lua, but its feature set is more experimental (e.g., mutable Lua enums). It serves as a reference for how reflection can underpin a bridge.

(5) Revised Implementation Idea – Lura

Key steps:

Adopt mature Lua‑bridge utilities

Leverage the reflection library for function type erasure

Wrap userdata with the reflection library’s UserObject

Below is a concrete example using the Vector3 class.

Registering Reflection Information

__register_type<rstudio::math::Vector3>(
"rstudio::math::Vector3"
)
//member fields export here
.property(
"x"
, &rstudio::math::Vector3::x)
.property(
"y"
, &rstudio::math::Vector3::y)
.property(
"z"
, &rstudio::math::Vector3::z)
.static_property(
"ZERO"
, [](){
return
rstudio::math::Vector3::ZERO; })
.constructor<double, double, double>()
.constructor<
const
rstudio::math::Vector3&>()
.constructor<double>()
.constructor<>()
.overload(
"__assign"
,[](rstudio::math::Vector3*
self
, double fScalar){
return
self
->operator=(fScalar); }
,[](rstudio::math::Vector3*
self
,
const
rstudio::math::Vector3& rhs){
return
self
->operator=(rhs); }
)
.function(
"Length"
, &rstudio::math::Vector3::Length)
;

Lua Registration

lura::get_global_namespace(L).begin_namespace("math3d")
    .begin_class<rstudio::math::Vector3>("Vector3")
    .end_class()
.end_namespace();

Compared to luabridge, the property and function registration is now handled by the reflection layer, reducing duplication.

Core Mechanisms of Lura

Lura’s implementation revolves around two meta‑tables:

Static‑member meta‑table (provides __index and __call for class‑level access)

Instance meta‑table (provides __index, __newindex, __gc, etc.)

Example of static‑member meta‑table creation:

lua_pushliteral(L, "__index");
lua_pushlightuserdata(L, (
void
*)&cls);
lua_pushvalue(L, -3);
lua_pushcclosure(L, LuaCFunctions::StaticMemberMetaIndex, 2);
lua_rawset(L, -3);

Example of instance meta‑table creation (including __index, __newindex, __gc):

lua_pushliteral(L, "__index");
lua_pushlightuserdata(L, (
void
*)&cls);
lua_pushvalue(L, clTableIndex);
lua_pushcclosure(L, InstanceMetaIndex, 2);
lua_rawset(L, -3);

lua_pushliteral(L, "__newindex");
lua_pushlightuserdata(L, (
void
*)&cls);
lua_pushvalue(L, clTableIndex);
lua_pushcclosure(L, InstanceMetaNewIndex, 2);
lua_rawset(L, -3);

lua_pushliteral(L, "__gc");
lua_pushcfunction(L, InstanceMetaGc);
lua_rawset(L, -3);

Construction of a C++ object from Lua:

int LuaCFunctions::InstanceMetaCreate(lua_State* L) {
    // Retrieve the MetaClass* from upvalue
    lua_pushvalue(L, lua_upvalueindex(1));
    const MetaClass* cls = (const MetaClass*)lua_touserdata(L, -1);
    lua_pop(L, 1);

    // Collect Lua arguments
    framework::reflection::Args args;
    const int nargs = lua_gettop(L) - 1; // first arg is the class table
    for (int i = 2; i < 2 + nargs; ++i) {
        args += LuraHelper::GetValue(L, i);
    }

    // Find matching constructor
    framework::reflection::UserObject obj;
    for (size_t i = 0, nb = cls->GetConstructorCount(); i < nb; ++i) {
        const auto& ctor = *(cls->GetConstructor(i));
        if (ctor.CheckLuaSignatureMatchs(L, 1, nargs)) {
            obj = ctor.CreateFromLua(L, nullptr, 1);
            break;
        }
    }
    if (obj == framework::reflection::UserObject::nothing) {
        luaL_error(L, "Matching constructor not found");
        return 0;
    }

    // Create userdata and bind instance meta‑table
    void* ud = lua_newuserdata(L, sizeof(UserObject));
    new (ud) UserObject(obj);
    const void* insMetaKey = cls->GetUserdata
();
    lua_rawgetp(L, LUA_REGISTRYINDEX, insMetaKey);
    lua_setmetatable(L, -2);
    return 1;
}

Small tips: use Lua up‑values to pass extra parameters (e.g., MetaClass pointer) into C++ callbacks, making the stack state explicit and easier to debug.

Additional Topics

C++ calling Lua functions (via lua_pcall ) – not detailed here.

Lua coroutine handling – Lura integrates a coroutine pool and works with existing C++ coroutine frameworks.

Profiler integration – Lura uses the commercial FramePro SDK; other profilers can be swapped in.

4. Other Script Bridges

While Lua bridges are representative, other dynamic languages have similar patterns: differing C APIs and language‑specific features (e.g., asymmetric coroutines in Lua). The core ideas of type erasure, property handling, and meta‑table design are reusable across languages.

Conclusion

By leveraging C++ reflection, implementing a Lua bridge becomes straightforward. Complex template code is relegated to the reflection layer, resulting in a clean, maintainable bridge that supports function calls, property access, object lifecycle, and advanced features like coroutines and profiling.

References:

GitHub Ponder library

Luabridge library

reflectionLuaCodeGenerationBridgeGameDevelopmentC++Metatable
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.