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.
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
Tencent Cloud Developer
Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.