C++
Lua
module
ffi
UserData

C++ & Lua : C++ から Lua へ提供するモジュールと C++ データ型と Lua ユーザーデータを簡潔に定義するヘルパーライブラリーの実装例

More than 1 year has passed since last update.

目的

前回投稿した「C++ & Lua : C++ のネイティブデータ型を Lua へ提供し、操作する例」 ではそれに必要な実装をコンパクトに直接済ませた。

この記事では前回の内容をヘルパーライブラリーとして整理する実装例を示す。(ヘルパーライブラリー実装の指針とする例であり、あくまでも実用的なライブラリーを実装するための足掛かりや類似のコード例について理解を深める目的の程度に留める。)

実装例

#include <lua.hpp>
#include <iostream>
#include <vector>
#include <new>
#include <mutex>
#include <stdexcept>

namespace usagi
{
  namespace lua
  {
    using function_definitions_type = std::vector< luaL_Reg >;

    struct meta_definition_type
    {
      std::string               name;
      function_definitions_type function_definitions;
    };

    using meta_definitions_type = std::vector< meta_definition_type >;

    template < typename T, typename ... TS >
    auto make_userdata( lua_State* raw_state, const std::string& name, TS ... ts )
    {
      const auto p = lua_newuserdata( raw_state, sizeof( T ) );
      luaL_setmetatable( raw_state, name.c_str() );
      return new( p ) T( ts ... );
    }

    template < typename T >
    auto check_userdata( lua_State* raw_state, const std::string& name, const int narg = 1 )
    {
      if ( const auto p = luaL_checkudata( raw_state, narg, name.c_str() ) )
        return reinterpret_cast< T* >( p );
      throw std::runtime_error( "check_userdata was failed." );
    }

    namespace detail
    {
      namespace require
      {
        const function_definitions_type* pfs;
        const meta_definitions_type*     pms;
        std::mutex                       m;
      }
    }

    auto require
    ( lua_State* raw_state
    , const std::string& name
    , const function_definitions_type& function_definitions
    , const meta_definitions_type& meta_definitions = { }
    , const bool is_global = true
    )
    {
      std::lock_guard< decltype( detail::require::m ) > lock( detail::require::m );
      detail::require::pfs = &function_definitions;
      detail::require::pms = &meta_definitions;

      luaL_requiref
      ( raw_state
      , name.c_str()
      , []( auto raw_state )
        {
          function_definitions_type function_definitions_copy( *detail::require::pfs );
          function_definitions_copy.resize( function_definitions_copy.size() + 1 );

          luaL_checkversion( raw_state );
          lua_createtable( raw_state, 0, function_definitions_copy.size() );
          luaL_setfuncs( raw_state, function_definitions_copy.data(), 0 );

          for ( auto meta_definition : *detail::require::pms )
          {
            function_definitions_type meta_function_definitions_copy( meta_definition.function_definitions );
            meta_function_definitions_copy.resize( meta_function_definitions_copy.size() + 1 );

            luaL_newmetatable( raw_state, meta_definition.name.c_str() );

            luaL_checkversion( raw_state );
            lua_createtable( raw_state, 0, meta_function_definitions_copy.size() );
            luaL_setfuncs( raw_state, meta_function_definitions_copy.data(), 0 );

            lua_setfield( raw_state, -2, "__index" );

            lua_pop( raw_state, 1);
          }

          return 1;
        }
      , static_cast< int >( is_global )
      );

      lua_pop( raw_state, 1 );
    }
  }
}

auto main() -> int
{
  const auto raw_state = luaL_newstate();
  luaL_openlibs( raw_state );

  usagi::lua::require
  ( raw_state
  , "usagi"
  , { { "make_x"
      , []( auto raw_state )
        { std::cout << "usagi.make_x : " << usagi::lua::make_userdata<int>( raw_state, "x_type", static_cast<int>( luaL_checkinteger( raw_state, 1 ) ) ) << "\n"; return 1; }
      }
    , { "make_y"
      , []( auto raw_state )
        { std::cout << "usagi.make_y : " << usagi::lua::make_userdata<int>( raw_state, "y_type", static_cast<int>( luaL_checkinteger( raw_state, 1 ) ) ) << "\n"; return 1; }
      }
    }
  , { { "x_type", { { "hello", []( auto raw_state ) { std::cout << "x_type.hello : " << *usagi::lua::check_userdata<int>( raw_state, "x_type" ) << "\n"; return 0; } } } }
    , { "y_type", { { "hello", []( auto raw_state ) { std::cout << "y_type.hello : " << *usagi::lua::check_userdata<int>( raw_state, "y_type" ) << "\n"; return 0; } } } }
    }
  );

  luaL_dostring
  ( raw_state
  , u8R"(
      print( "hello, lua" )
      local x = usagi.make_x( 123 )
      local y = usagi.make_y( 456 )
      x:hello();
      y:hello();
    )"
  );

  lua_close( raw_state );
}

実行結果

hello, lua
usagi.make_x : 0x16443e8
usagi.make_y : 0x1644428
x_type.hello : 123
y_type.hello : 456

Wandbox

概説

  • usagi::lua 名前空間に Lua 制御のためのヘルパーライブラリーを構築すると仮定
  • usagi::lua::require により Lua 側にモジュール的な機能とユーザー定義型を内包可能なユーザーデータ型をまとめて提供する
    • 第2仮引数 name : Lua 側へ与えるモジュールの名称
    • 第3仮引数 function_definitions : Lua モジュールへ直接実装するメンバー関数群に相当する定義群
    • 第4仮引数 meta_definitions : Lua モジュールが提供するユーザー定義型とそのメンバー関数群に相当する定義群
    • 第5仮引数 is_global : モジュールをグローバル変数に登録するか否か
  • usagi::lua::make_userdata ヘルパー関数により C++ の任意の型をバインディングする Lua ユーザーデータを簡便に定義できる
  • usagi::lua::check_userdata ヘルパー関数により Lua ユーザーデータにバインディングされた C++ のデータ型を簡便に取得できる

トリック

  • usagi::lua::requireusagi::lua::detail 名前空間に定義した補助的な大域変数を用いて本来ステートレスなファンクターしか扱えない Lua の C API の lua_CFunction 型に与えるファンクターに間接的にステートを与え function_definitionsmeta_definitions を参照せしめている。
  • luaL_newlib はマクロによる便利なヘルパー API ではあるが、これを使うと展開される luaL_newlibtable マクロがサイズ付きの配列(int a[]のような)を前提としてしているため、C++ 的には見方によってあまり美しくないし、ユーザーがこの配列に明示的にいわゆる"番兵"(配列末尾の nullptr 値のような)を与えなくてはならない事も見方によってはあまり美しくない。そこで低レベル API を直接展開して用いる事で、見方によっては美しい C++ ライブラリーのそとっつらを整えている。
    • 「見方によっては」としつこく書いたが、この実装例は明らかにデータ操作上は不要なオーバーヘッドが発生するからあらゆる視点で美しいとは言い難いが、低レベル API を封入し利便性と可読性の良いユーザーコードを記述するためのライブラリーとして考えると、特にこの機能は一般に繰り返して実行されたり、ユーザーを著しく待たせるほどの重い処理では無い事もあり、見方によってはこの実装もまた美しくもありえる。
#define luaL_newlibtable(L,l)        \
  lua_createtable(L, 0, sizeof(l)/sizeof((l)[0]) - 1)

#define luaL_newlib(L,l)  \
  (luaL_checkversion(L), luaL_newlibtable(L,l), luaL_setfuncs(L,l,0))

typedef struct luaL_Reg {
  const char *name;
  lua_CFunction func;
} luaL_Reg;

LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);