This module embeds a Lua runtime against a catalog of reflected C++ types, so a caller runs Lua source that constructs those types, calls their methods, and reads their properties without writing Lua C API glue for each binding. It is not a wrapper whose unit of work is a raw lua_State. The unit of work is a Script, which loads source into a Context that already holds the bindings.
A TypeManager is the catalog. It is a Pt::Reflex::TypeManager that already knows the scalar types Lua can convert (int, long, float, double, bool, and string), void, and AsyncCall*. User types and functions are registered with the Reflex API before a Context is constructed. How a C++ type declares constructors, methods, and properties is the Reflex module; this module binds that catalog into Lua.
Context opens a Lua state, loads the standard libraries, and binds every eligible type and asynchronous function in the catalog, including entries that live on a parent type manager. A bound type becomes a Lua global of that type's name. Calling the global constructs a userdata instance. Methods use the Lua method syntax, and properties are fields. Scalar arguments and results convert to and from Lua booleans, numbers, and strings. A result that is a registered object is copy-constructed into userdata that Lua owns.
Script loads source as a coroutine of that context. Only one script may use a context at a time. The script is a Selectable. Blocking advance() runs it on the calling thread until the next yield, native call, success, or error. Asynchronous work attaches the script with setActive() so an EventLoop can drive beginAdvance() and endAdvance(), and advanced() reports each step. The loop does not own the script: the code that creates it keeps it alive while a step is still waiting. A C++20 awaitable wraps the same asynchronous step.
When Lua calls a reflected method, property, or constructor, the script reports NativeCall and runs a Call. When Lua calls a function or method that returns AsyncCall*, the script starts that call on the event loop and resumes when the call is ready. Blocking advance() cannot start an asynchronous call.
After ScriptOk, Result reads a named Lua global from the context's state. A name that is already bound at construction throws std::logic_error. A script or native failure is ScriptError, and errorMessage() holds the text.
The example registers a Point type, binds it, runs a chunk that constructs a point and calls sum, and reads the result.
The rest of this chapter is the runtime and bindings, then scripts, then native calls.
This chapter covers:
Runtime work is a catalog, then a bind. TypeManager is the catalog: a Pt::Reflex::TypeManager that already registers the scalar types Lua can convert, void, and AsyncCall*. Register user types and asynchronous functions on that manager, or on a parent manager it can see, before a Context is constructed. The context does not watch the catalog afterwards.
Context is the bind step. It opens a Lua state, loads the standard libraries, and walks the type manager, including any parent, to install globals. A type is bound when it has at least one constructor, method, or property. Its name becomes a callable global that constructs userdata. Methods are looked up on the instance, properties are fields, and Reflex methods named +, -, , /, ==, <, <=, or # become the matching Lua metamethods. A free function is bound only when it returns AsyncCall; a synchronous free function stays in the catalog and does not become a Lua global.
The context does not own the type manager. The manager, and every type and function registered on it, must outlive the context. The context owns the Lua state and is not copyable. state() is that Lua state, for Result and for the Lua C API. reset() removes globals that are neither bindings nor standard-library names, then collects garbage, so the same context can load another script after the previous Script has been destroyed.
A name that is already a binding or a standard Lua global throws std::logic_error when a different type or function would take it. Binding the same type or function twice is ignored. Types in a parent manager are visible to a child; two different types that share a name across that chain fail at context construction.
TypeManager is the Lua catalog the runtime group described. It is a Pt::Reflex::TypeManager that already registers int, long, float, double, bool, string, void, and AsyncCall*. Those builtins exist so argument conversion and asynchronous calls have types before any user type is added.
Register user types and functions with the inherited Reflex operations before a Context is constructed. The manager does not own those user types. A parent type manager is visible to binding; put shared types on the parent and Lua-specific functions on a child if the same catalog serves more than one context.
voidType() is the void entry of the builtin set, used when an asynchronous call has no result. The manager is not copyable.
Context is the bind step of the runtime model. It opens a Lua state, loads the standard libraries, and installs globals for every eligible type and asynchronous function in tm, including a parent type manager. The manager is not owned; it must outlive the context, together with every type and function registered on it.
A type becomes a Lua class when it has a constructor, a method, or a property. The type name is a callable global. An instance is userdata that Lua owns. Methods use the Lua method syntax, properties are fields, and selected Reflex operator names map to metamethods. Scalar values convert in both directions. A returned object is copy-constructed into userdata; a type without a copy constructor cannot be returned by value.
A free function is bound only when it returns AsyncCall*. Two different types or functions that share a name, or a name that is already a standard Lua global, throw std::logic_error. Binding the same object twice is ignored.
state() is the Lua state, for Result and for the Lua C API. reset() drops script-created globals and collects garbage; the bindings remain. Destroy the Script first. The context is not copyable.
A Script loads a C string of Lua source into a Context and runs it as a coroutine of that context. Only one script may use a context at a time; a second construction throws std::logic_error. A syntax error does not throw: the script is ScriptError, errorMessage() holds the compiler text, and the context can be used again.
The script is a Selectable. Blocking advance() runs on the calling thread and returns at the next step. Yield means the coroutine yielded so other work can run; call advance() again. NativeCall means Lua invoked a reflected method, property, or constructor; the next advance() performs that Call and resumes. ScriptOk means the chunk finished. ScriptError means the chunk or a native call failed. A loop that continues on Yield and NativeCall, and stops on ScriptOk or ScriptError, is the usual blocking path.
Blocking advance() cannot start an AsyncCall. A function or method that returns AsyncCall* needs the event-loop path: setActive() on an EventLoop, beginAdvance() to start a step, endAdvance() in the advanced() slot, and another beginAdvance() while the status is Yield or NativeCall. The loop does not own the script. Keep the script alive until no step is waiting. cancel() aborts a pending step and any active async call.
After ScriptOk, Result reads a named global from the context's Lua state as an int. Construct it with Context::state(). C++20 advanceAsync() is the same asynchronous step as an awaitable: co_await script.advanceAsync() returns the Status of that step. Only one awaiter may be pending on a script.
Script is the coroutine that runs Lua source in a Context. The source is a C string loaded at construction. Only one script may use a context at a time; a second construction throws std::logic_error. A syntax error does not throw: the status is ScriptError, errorMessage() holds the compiler text, and the context is free for another script.
The script is a Selectable. Blocking advance() runs on the calling thread and returns at the next step. Continue while the status is Yield or NativeCall. Yield is a cooperative pause so other work can run. NativeCall means Lua invoked a reflected method, property, or constructor; the next advance() performs that Call and resumes. Stop on ScriptOk or ScriptError. Blocking advance() throws if an AsyncCall is pending.
Asynchronous work attaches the script with setActive() on an EventLoop. beginAdvance() starts a step, advanced() is emitted when the step completes, and endAdvance() returns the status. Call beginAdvance() again while the status is Yield or NativeCall. The loop does not own the script. Keep it alive until no step is waiting. cancel() aborts a pending step and any active async call.
C++20 advanceAsync() is that asynchronous step as an awaitable. fromState() finds the script stored in a Lua registry, for Lua C API code that needs it. The script is not copyable.
After a Script has reached ScriptOk, Result reads a named Lua global as an int from that context's state. Construct it with Context::state() and call get() with the global name the script assigned. A missing or non-numeric global converts as Lua does for integers, typically to zero.
The result does not own the state. The context that produced the state must outlive the result.
AsyncAdvance is the C++20 form of beginAdvance(), endAdvance(), and advanced(). co_await script.advanceAsync() runs one step and await_resume() returns that step's Status. Continue awaiting while the status is Yield or NativeCall.
Only one awaiter may be pending on a script. A second construction throws std::logic_error. Destroying a pending awaiter cancels the script. Destroying the script detaches the awaiter.
Lua reaches C++ through the Reflex catalog the context bound. A method, property, or constructor becomes a Call. The script yields, reports NativeCall, runs that call, and pushes the result back to Lua. The script owns the Call. Application code does not construct MethodCall, PropertyGetCall, PropertySetCall, or ConstructorCall.
A function or method whose return type is AsyncCall* is an asynchronous native call. Lua invokes it, the function returns a heap AsyncCall, and the script takes ownership. The script must be attached to an EventLoop. It binds the call, starts it with the loop, and resumes when finished() is emitted. Blocking advance() throws if such a call is pending.
Implement async work by subclassing BasicAsyncCall. For a result type R, override onBeginCall() to start work on the loop, onResult() to produce R, and onCancel() to abort. For void, there is no onResult(). Call setReady() when the work is done, or setError() if it failed. AsyncFunction is a Reflex function helper whose return type is already AsyncCall*.
The example is a timer wait registered as a Lua global. The script must use beginAdvance() on an event loop.
AsyncCall is native work that Lua starts and that finishes later on an EventLoop. A reflected function or method whose return type is AsyncCall* returns a heap instance; the Script takes ownership, binds it, starts it, and resumes when finished() is emitted.
Subclass BasicAsyncCall rather than this type. The script calls bind() with the context's type manager so rtype() is known, then beginAdvance() on the loop, which reaches onBeginCall(). When the work is done, setReady() emits finished(). setError() records a failure instead. The script then calls getResult() and pushes it to Lua, or becomes ScriptError. cancel() reaches onCancel().
BasicAsyncCall looks up typeid(R) at bind time. Override onBeginCall() to start work on the loop, onResult() to produce R, and onCancel() to abort. Call setReady() or setError() when the work ends.