C++20 + Lua = Flexibility

Jim (James) Pascoe
http://www.james-pascoe.com
james@james-pascoe.com

http://jamespascoe.github.io/accu2021
https://github.com/jamespascoe/LuaChat.git

Overview

A Tutorial on Combining C++20 and Lua 5.4.2
Up-to-date practical advice with code

Why Combine C++ and Lua?

Flexibility Post Release

  • Behaviour can be modified after code is shipped
    • Cope with future unknowns proactively
  • Modifications are fast
    • No compile, package, deploy cycle
  • Barrier to entry is much lower for Lua
    • Appeals to FAEs, Architects, Customers

High Speed Transport

Blu Wireless

  • IP networking over 5G mmWave (60 GHz) modems
    • 802.11ad MAC + PHY (Hydra) + software
  • High-bandwidth, low latency mobile Internet
    • Up to 3.5Gbps wireless links (up to 1km)
  • Embedded quad-core ARMv8 NPUs
    • Track-side / train-top mmWave radios

Connection Management

  • Mission critical software component
  • Decides which radio to connect to and when
  • v1.0 fixed behaviour: connect to strongest signal
  • Anomalies led to poor performance
  • Software updates were costly
  • Improvements could not be made fast enough

Mobile Connection Manager

  • Complete redesign
  • Decoupled architecture (C++17 & Lua 5.4.2)
  • Actions (C++): 'Scan', 'Connect', 'Probe' etc.
  • Behaviours (Lua): implement 'beam choreography'
  • Changes can be made in the field by FAEs
  • Consolidated into supported releases

Combining Modern C++ and Lua

Lua

  • Lightweight embeddable scripting language
  • Dynamically typed, runs by interpreting bytecode
  • Simple procedural syntax
  • Emphasis on meta-mechanisms
  • Instant appeal for Architects, FAEs etc.

The Lua C API

  • Lua communicates with C++ through a virtual stack
  • Strict stack discipline is not enforced
    • indices ≥ 1 are positions from the bottom
    • negative indices are relative to the top
  • Pseudo indices for the Lua Registry and Upvalues
  • Compile with LUA_USE_APICHECK to enable checks

Lua C API: Lua


-- Create a global table 't'
t = { x=1, y=2 }

function f (str, val, int)
  print(
    string.format(
      "Lua: f called with args: %s %d %d", str, val, int
    )
  )

  -- Call a C++ function
  local rc = cppFunc(str, t.y, int)

  return rc
end
            

Lua C API: C++


#include <iostream>
#include "lua.hpp"

int cppFunc(lua_State *L) {
  std::cout << "cppFunc called with args:" << std::endl;

  for (int n=1; n <= lua_gettop(L); ++n)
    std::cout << lua_tostring(L, n) << std::endl;

  return 0;
}

int main([[maybe_unused]] int argc, char ** argv)
{
  // Create a new lua state
  lua_State *L = luaL_newstate();

  // Open all libraries
  luaL_openlibs(L);

  // export a C++ function to Lua
  lua_register(L, "cppFunc", cppFunc);

  // Load and run the Lua file
  luaL_loadfile(L, argv[1]);
  lua_pcall(L, 0, 0, 0);

  // Call 'f' with the arguments "how", t.x, 14
  lua_getglobal(L, "f");     /* function to be called */
  lua_pushliteral(L, "how"); /* 1st argument */
  lua_getglobal(L, "t");     /* table to be indexed */
  lua_getfield(L, -1, "x");  /* push t.x (2nd arg) */
  lua_remove(L, -2);         /* remove 't' from the stack */
  lua_pushinteger(L, 14);    /* 3rd argument */
  lua_call(L, 3, 1);         /* call 'f' (3 args, 1 res) */

  lua_close(L);
}
            

Lua C API: Build & Run


> brew install lua # sudo apt-get -y install lua5.4
> clang++ -std=c++2a -llua -o lua-cpp lua-cpp.cpp
> ./lua-cpp lua-cpp.lua
Lua: f called with args: how 1 14
cppFunc called with args:
how
2
14
              

Sol3: Binding Modern C++ and Lua

Sol3

Sol3: Stack Manipulation


#include <iostream>

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>

int cppFunc(lua_State *L) {
  std::cout << "cppFunc called with args:" << std::endl;

  for (int n=1; n <= lua_gettop(L); ++n)
    std::cout << lua_tostring(L, n) << std::endl;

  return 0;
}

int main([[maybe_unused]] int argc, char ** argv)
{
  // Create a new lua state and open libraries
  sol::state lua;
  lua.open_libraries(sol::lib::base, sol::lib::string);

  // Export a C++ function to Lua
  lua["cppFunc"] = cppFunc;

  // Load and run the Lua file
  lua.script_file(argv[1]);

  // Call 'f' with the arguments "how", t.x, 14
  sol::function f = lua["f"];
  f("how", lua["t"]["x"], 14);
}
            

Sol3: Improved Example


#include <iostream>

#define SOL_ALL_SAFETIES_ON 1
#include <sol/sol.hpp>

int cppFunc_oneline(std::string str, int a, int b) {
  std::cout << "cppFunc_oneline called with args: " <<
    str << " " << a << " " << b << std::endl;

  return 0;
}

int main([[maybe_unused]] int argc, char ** argv[1]);
{
  // Create a new lua state and open libraries
  sol::state lua;
  lua.open_libraries(sol::lib::base, sol::lib::string);

  // Export a C++ function to Lua
  lua["cppFunc"] = cppFunc_oneline;

  // Load and run the Lua file
  lua.script_file(argv[1]);

  // Call 'f' with the arguments "how", t.x, 14
  sol::function f = lua["f"];
  f("how", lua["t"]["x"], 14);
}
            

Sol3 Example: Build & Run


> brew install lua # sudo apt-get -y install lua5.4
> git clone https://github.com/ThePhD/sol2.git
> clang++ -std=c++2a -I sol2/include -llua -o lua-sol3 lua-sol3.cpp
> ./lua-sol3 lua-cpp.lua
Lua: f called with args: how 1 14
cppFunc called with args:
how
2
14
              

> clang++ -std=c++2a -I sol2/include -llua -o lua-sol3 lua-sol3-ol.cpp
> ./lua-sol3 lua-cpp.lua
Lua: f called with args: how 1 14
cppFunc_oneline called with args: how 2 14
              

Sol3: Container Example


              
            

Sol3 Container Example: Build & Run


> git clone https://github.com/HowardHinnant/date.git
> git clone https://github.com/ThePhD/sol2.git
> clang++ -std=c++2a -I sol2/include/ -I date/include/date -l lua -o container container.cpp
> ./container
Lua: 21:35:10.437971 msg 1
Lua: 21:35:10.438393 msg 2
Lua: 21:35:10.438403 msg 3
C++: 21:35:10.437971 msg 1
C++: 21:35:10.438393 msg 2
C++: 21:35:10.438403 msg 3
              

Next Steps

  • What other features does Sol3 support?
    • optionals, callables, user-types, concurrency
    • lots more - feature matrix available here
  • Customisation Traits
    • Containers, reference-counted resources, UDTs
  • Further examples
    • Comprehensive selection in examples directory

SWIG and LuaChat

SWIG

  • Simplified Wrapper and Interface Generator
  • Produces C++ bindings for many target languages
  • Generates Lua stack calls for std C++ types
    • std::string, std::vector, std::map etc.
  • C++20 types can be supported with typemaps
  • Integrates well with CMake

LuaChat

  • Unix 'talk' program (written in C++17 & Lua 5.3/5.4)
  • Available on GitHub (MIT license)
  • Asio for asynchronous TCP and timers
  • spdlog for logging, cxxopts for command line processing and CMake for build generation

Build Instructions

Ubuntu 18.04 (Linux Mint 19):


git clone https://github.com/jamespascoe/LuaChat.git
sudo apt-get -y install lua5.3 lua5.3-dev luarocks swig
sudo luarocks install luaposix
mkdir build; cd build; cmake ../LuaChat; make
./src/lua_chat
            

MacOS (Big Sur):


git clone https://github.com/jamespascoe/LuaChat.git
brew install lua luarocks swig
luarocks install luaposix
mkdir build; cd build; cmake ../LuaChat; make
./src/lua_chat
            

LuaChat SWIG CMake


set(LUA_CHAT_SWIG_SRCS
    lua_chat_actions.i lua_chat_action_log.cpp)

set_source_files_properties(${LUA_CHAT_SWIG_SRCS}
                            PROPERTIES CPLUSPLUS ON)

swig_add_library(actions TYPE USE_BUILD_SHARED_LIBS
                 LANGUAGE lua
                 SOURCES ${LUA_CHAT_SWIG_SRCS})

target_include_directories(actions PRIVATE
                           ${CMAKE_CURRENT_SOURCE_DIR}
                           ${LUA_CHAT_SOURCE_DIR}/src
                           ${LUA_CHAT_SOURCE_DIR}/third_party
                           ${LUA_INCLUDE_DIR})

target_compile_definitions(actions PRIVATE ASIO_STANDALONE)

target_link_libraries(actions PRIVATE std::filesystem)
            

LuaChat SWIG Input


%module Actions

%include <std_string.i>

// Definitions required by the SWIG wrapper to compile
%{
#include "lua_chat_log_manager.hpp"
#include "lua_chat_action_log.hpp"
#include "lua_chat_action_talk.hpp"
#include "lua_chat_action_timer.hpp"
%}

// Files to be wrapped by SWIG
%include "lua_chat_action_log.hpp"

%define CTOR_ERROR
{
  try {
    $function
  }
  catch (std::exception const& e) {
    log_fatal(e.what());
  }
}
%enddef

// Include the actions and define exception handlers
%exception Talk::Talk CTOR_ERROR;
%include "lua_chat_action_talk.hpp"

%exception Timer::Timer CTOR_ERROR;
%include "lua_chat_action_timer.hpp"
            

Typemaps

  • Maps C++ types onto types in the target language
  • We can add support for Modern C++ abstractions
    • E.g. callbacks: Lua functions → std::function
  • Acknowledgement: thanks to Petar Terziev for the original version of the following example

Lua Callback: SWIG Typemap


%typemap(typecheck) Example::Callback & {
  $1 = lua_isfunction(L, $input);
}

%typemap(in) Example::Callback & (Example::Callback temp) {
  // Create a reference to the Lua callback
  SWIGLUA_REF fn;
  swiglua_ref_set(&fn, L, $input);

  temp = [&fn]() {
    swiglua_ref_get(&fn);

    lua_pcall(fn.L, 0, 0, 0);
  };

  $1 = &temp;
}

// %include source files AFTER typemap declarations
            

Actions

LuaChat Actions

  • Talk: sends messages to a remote LuaChat
    • Based on Asio - must also act as a server
    • Use TCP for fault-tolerant in-order delivery
    • One asynchronous TCP connection per message
  • Timer: implements blocking and non-blocking waits
    • Use Asio - required for Lua coroutine dispatcher
  • Log: wraps spdlog primitives

TCP Connections


class tcp_connection
{
public:
  using pointer = std::shared_ptr<tcp_connection>

  static pointer create(asio::io_context& io_context) {
    return pointer(new tcp_connection(io_context));
  }

  asio::ip::tcp::socket& socket() { return m_socket; }

  std::string& data() { return m_data; }

private:
  tcp_connection(asio::io_context& io_context)
    : m_socket(io_context) {}

  asio::ip::tcp::socket m_socket;
  std::string m_data;
};
            

Connection Handling


Talk::Talk(unsigned short port)
    : m_acceptor(m_io_context,
                tcp::endpoint(tcp::v4(), port)) {
  start_accept();

  m_thread = std::thread([this](){ m_io_context.run(); });

  log_trace("Talk action starting");
}

void Talk::start_accept() {
  tcp_connection::pointer connection =
      tcp_connection::create(
        m_acceptor.get_executor().context()
      );

  m_acceptor.async_accept(connection->socket(),
      [this, connection](const asio::error_code& error) {
        handle_accept(connection, error);
      }
  );
}
            

Accepting Connections


void Talk::handle_accept(tcp_connection::pointer connection,
                         asio::error_code const& error) {
  if (!error) {
    log_debug("Accepted message connection");

    asio::async_read(
      connection->socket(),
      asio::dynamic_buffer(connection->data()),
      [this, connection](
        const asio::error_code& error,
        std::size_t bytes_transferred)
        {
          handle_read(error, bytes_transferred, connection);
        }
    );
  } else
    log_error("Talk accept failed: {}", error.message());

  start_accept();
}
            

Storing Data


void Talk::handle_read(asio::error_code const& error,
                       std::size_t bytes_transferred,
                       tcp_connection::pointer connection) {
  // Check error - 'eof' means remote connection closed
  if (!error || error == asio::error::eof) {

    // Limit the message array
    if (m_messages.size() > max_messages)
      m_messages.erase(m_messages.begin());

    // Store the message for Lua retrieval
    m_messages.emplace_back(connection->data());

    log_info("Received message ({} bytes): {}",
             bytes_transferred,
             connection->data());
  } else
    log_error("Talk read failed: {}", error.message());
}
            

Lua Retrieval


std::string Talk::GetNextMessage(void) {
  if (!IsMessageAvailable())
    return "";

  std::string ret = m_messages.front();

  m_messages.erase(m_messages.begin());

  return ret;
}
            

Behaviour

Coroutines

  • Great for event-driven asynchronous systems
  • Lua coroutines are stackful
  • C++20 coroutines are stackless
  • Single threaded so lock-free, no races etc.
  • Implement your own dispatcher in Lua

LuaChat Behaviour

  • Sender coroutine: sends user input to peer
  • Receiver coroutine: prints received messages
  • Dispatcher coroutine: schedules sender and receiver
  • main: processes arguments and creates coroutines

Sender Coroutine


function sender (talk, host, port)

  while true do

    local ret = require 'posix'.rpoll(0, 1000)
    if (ret == 1) then
      local message = io.read()
      if (message ~= "") then

        local ret = talk:Send(
          tostring(host), tostring(port), tostring(message)
        )

        if (ret == Actions.Talk.ErrorType_SUCCESS) then
          Actions.Log.info(
            string.format(
              "Message sent to %s:%s %s", host, port, message
            )
          )
        end
      end
    end

    coroutine.yield()

  end

end
            

Receiver Coroutine


function receiver (talk, host, port)

  while true do

    -- Yield until a message arrives, at which point, print it
    repeat
      coroutine.yield()
    until talk:IsMessageAvailable()

    local message = talk:GetNextMessage()

    Actions.Log.info(
      string.format(
        "Received from %s:%s %s", host, port, message
      )
    )

    print(host .. ":" .. tostring(port) .. "> " .. message)

  end

end
            

Dispatcher


function dispatcher (coroutines)

  local timer = Actions.Timer()

  while true do
    if next(coroutines) == nil then break end -- no coroutines

    for name, co in pairs(coroutines) do
      local status, result = coroutine.resume(co)

      if result then -- coroutine has exited

        if type(result) == "string" then -- runtime error
          Actions.Log.critical(
            "Coroutine '" .. tostring(name) .. "' error " .. result
          )
        else
          Actions.Log.warn(
            "Coroutine '" .. tostring(name) .. "' exited"
          )
        end

        coroutines[name] = nil
      end
    end

    timer(Actions.Timer.WaitType_BLOCK, 1, "ms", 0xffffffff)

  end
end
            

Performance

Measurement

How do we compare performance?

Lua Bindings by Compiler (x86-64 i5-6200u)

Select Lua Bindings: x86-64 i5-6200u

Select Lua Bindings: Embedded ARMv8

Aggregated Bindings By Architecture

Conclusion

Performance Advice

  • Sol3 is fast but you can go faster
    • lots of good advice here
  • MCM spends a lot of time in the SWIG wrapper
    • prefer lightweight typemaps
  • The partition between C++ and Lua is important
    • as is the concurrency design
  • How the code interacts with Lua is significant
    • prefer pre-compiled long-lived behaviours

Conclusion

  • The combination of C++ and Lua is powerful
    • Actions (C++) and Behaviours (Lua)
  • Sol3 binds Modern C++ to Lua
    • simple-to-use, fast, ideal for Modern C++
    • by definition is a C++ to Lua binding
  • SWIG allows us to map C++ types to/from Lua
    • generates bindings in many languages
    • be mindful of performance
  • Lua 5.4.2 is now available

Questions?

http://www.james-pascoe.com
james@james-pascoe.com

http://jamespascoe.github.io/accu2021
https://github.com/jamespascoe/LuaChat.git