2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AviUtlでC++のクラスや構造体を使う

Last updated at Posted at 2022-03-01

AviUtlではLuaを使用したスクリプティングが可能ですが、Luaの機能を使ってC++のクラスを変数として使用します。

準備

まずそもそも、LuaからC++の関数を使用するためにはDLLが必要です。
以前にこちらでも記述したので詳細は省きますが、以下のような手順をとります。

  1. Visual Studio 2019でC++のプロジェクトを作成する。
  2. 構成の種類ターゲットファイルの拡張子dllに設定。
  3. Lua5.1の中にあるlua51.libをライブラリとして指定する。またlua.hppのある場所をインクルードディレクトリに設定する。
  4. 文字セットマルチバイト文字セットを使用するに設定する。
  5. C++のソースファイルを作成し、以下のようなコードを書く。
main.cpp
#include <lua.hpp>

int test_func(lua_State* L){
    //ここにコード

    return 0;
}

static luaL_Reg functions[] = {
    {"test_func", test_func},
    { nullptr, nullptr }
};

extern "C" {
    __declspec(dllexport) int luaopen_test_module(lua_State* L) {
        luaL_register(L, "test_module", functions);
        return 1;
    }
}

userdataとは

Luaにはユーザーデータ(userdata)という型があり、これはC言語のメモリを扱っています。
Lua標準のライブラリでは、fs = io.open("hoge.txt", "r")したときのfsがユーザーデータ型です。

Luaの標準ライブラリから学ぶ

これはLuaのソースコードを見れば一目瞭然です。

liolib.c
static int io_open (lua_State *L) {
  const char *filename = luaL_checkstring(L, 1);
  const char *mode = luaL_optstring(L, 2, "r");
  FILE **pf = newfile(L);
  *pf = fopen(filename, mode);
  return (*pf == NULL) ? pushresult(L, 0, filename) : 1;
}

このファイルハンドルfsは、fopenしたときの返り値であるFILE*型であることがわかります。

ここでnewfile(L)を調べてみると、

liolib.c
static FILE **newfile (lua_State *L) {
  FILE **pf = (FILE **)lua_newuserdata(L, sizeof(FILE *));
  *pf = NULL;  /* file handle is currently `closed' */
  luaL_getmetatable(L, LUA_FILEHANDLE);
  lua_setmetatable(L, -2);
  return pf;
}

のようになっていることがわかります。
FILE*型のポインタをlua_newuserdata関数で取得していることがわかります。

実際にクラスを作る

今回はC++のstd::complexを使用して、複素数を表現できるクラスを作成します。

定義

複素数クラスを得るためのLua関数をnew_complex関数とします。
先程のlua_newuserdata関数を使用するのですが、この関数は指定したサイズのメモリを確保し、それをスタックに乗せるという関数です。
mallocに似ていますが、Lua側でメモリの管理が行なえます。

main.cpp
#include <complex>

typedef std::complex<double> Complex;

int new_complex(lua_State* L){   //Luaから呼び出す関数
   //引数を取得
   double r = lua_tonumber(L, 1); 
   double i = lua_tonumber(L, 2);
   
   // コンストラクタ
   Complex* p = lua_newuserdata(L, sizeof(Complex));
   auto c = new(p) Complex(r, i);

   return 1;
}

クラスを引数として使う

続いて、作成したクラスをLua関数の引数として使用します。
今回は共役複素数を得るconj関数を作成してみます。

userdataはlua_userdata関数から取得できます。

main.cpp
typedef std::complex<double> Complex;

int complex_conj(lua_State* L){   //Luaから呼び出す関数
   //スタックからuserdataを取得
   Complex* val = reinterpret_cast<Complex*>( lua_touserdata(L,1) );
   
   //返り値に使用するクラスのメモリを確保
   Complex* ret = reinterpret_cast<Complex*>( lua_newuserdata(L, sizeof(Complex)) );
   ret->real(0);   
   ret->imag(0);

   //共役を得る
   *ret = std::conj(*val);

   //スタックにあるuserdataを渡す
   return 1;
}

metatableの活用

Luaにはメタテーブル(metatable)という機能が存在します。
metatableはテーブル型の変数で、未定義の演算を定義したりすることができます。
userdataにもmetatableを設定することができ、クラスから関数を呼び出したり、クラス同士の四則演算を定義するなどできます。

まず、DLLが呼び出された時点でメタテーブルをグローバルに登録しておきましょう。

main.cpp
#define METATABLE_NAME "std::complex<double>" //メタテーブルの名前(なんでもいい)
typedef std::complex<double> Complex;

static luaL_Reg meta_functions[] = {
   //メタテーブルに登録する関数
   {nullptr, nullptr}
}

extern "C" {
    __declspec(dllexport) int luaopen_test_module(lua_State* L) {
        luaL_register(L, "test_module", functions);

        //メタテーブルを作成し関数を登録
        luaL_newmetatable(L, METATABLE_NAME); 
        luaL_register(L, NULL, meta_functions);

        //メタテーブルの__indexに関数を登録(complex:conj()のように呼び出せるようになる)
        lua_pushstring(L, "__index");
		lua_newtable(L);
        luaL_register(L, NULL, meta_functions);
		lua_settable(L, -3);
        
        //メタテーブルをスタックから取り除く
        lua_pop(L, 1); 
        
        return 1;
    }
}

続いて、Lua用関数側でメタテーブルを複素数クラスに設定していきます。

main.cpp
#define METATABLE_NAME "std::complex<double>"
typedef std::complex<double> Complex;

int new_complex(lua_State* L){   //Luaから呼び出す関数
   double r = lua_tonumber(L, 1); 
   double i = lua_tonumber(L, 2);
   
   // コンストラクタ
   Complex* c = lua_newuserdata(L, sizeof(Complex));
   auto c = new(p) Complex(r, i);

   //メタテーブルを設定
   luaL_getmetatable(L, METATABLE_NAME);  //メタテーブルをスタックに積む
   lua_setmetatable(L, -2); //メタテーブルをuserdataに設定

   return 1;
}

これでメタテーブルの設定は完了です。

luaL_checkudataを使用する

luaL_checkudata関数を使用すると、指定したメタテーブルをユーザーデータが保持しているかを確認することができます。
保持していれば、そのユーザーデータをスタックに積みます。
この関数のマクロをcomplex_checkと設定して、以下のように変更してみました。

main.cpp
#define METATABLE_NAME "std::complex<double>"
#define complex_check(L, n) (reinterpret_cast<std::complex<double>*>(luaL_checkudata(L, n, METATABLE_NAME)))

typedef std::complex<double> Complex;

int complex_conj(lua_State* L){   //共役複素数を得る関数
   //引数を取得
   Complex* val = complex_check(L, 1);

   //共役を取得
   auto ret = std::conj(*val)
   
   //返り値用のクラス
   auto p = lua_newuserdata(L, sizeof(Complex));
   auto c = new(p) Complex(ret.real(), ret.imag());

   //メタテーブル設定
   luaL_getmetatable(L, METATABLE_NAME);
   lua_setmetatable(L, -2);

   return 1;
}

metatableを使用した演算の定義

メタテーブルを使用すると、+*<=のような記号を使った演算をも定義することができます。
例えば、加算なら__add、減算なら__subという名前のキーでメタテーブルに登録します。
今回は掛算を表す__mulと、tostring関数を使用したときの出力を定義する__tostring関数を作成してみます。
ちなみに、このような__hogehogeをメタメソッドと呼びます(参考)。

main.cpp
#include <iostream>
#include <string>

#define METATABLE_NAME "std::complex<double>"
#define complex_check(L, n) (reinterpret_cast<std::complex<double>*>(luaL_checkudata(L, n, METATABLE_NAME)))

typedef std::complex<double> Complex;

int complex__mul(lua_State* L){   //掛算の定義
   //スタックからuserdataを取得
   Complex* val1 = complex_check(L, 1);
   Complex* val2 = complex_check(L, 2);
   
   //返り値に使用するクラスのメモリを確保
   Complex* ret = reinterpret_cast<Complex*>( lua_newuserdata(L, sizeof(Complex)) );
   ret->real(0);   
   ret->imag(0);

   //メタテーブルを設定
   luaL_getmetatable(L, METATABLE_NAME);
   lua_setmetatable(L, -2);

   //掛算
   *ret = (*val1) * (*val2);

   //スタックにあるuserdataを渡す
   return 1;
}

int complex__tostring(lua_State* L){   //tostringの定義
   //スタックからuserdataを取得
   Complex* val = complex_check(L, 1);

   //文字列を作成
   std::string ret = "(" + std::to_string(val->real()) + "," + std::to_string(val->imag()) + ")";
   lua_pushstring(L, ret.c_str());

   //スタックにある文字列を渡す
   return 1;
}

ガーベッジコレクション

Luaはメモリ管理を、ガーベッジコレクションという仕組みで行います。
__gcメソッドを持っていると、メモリ解放時に呼び出してくれます。ここでメモリを開放します。

main.cpp
int complex__gc(lua_State* L){   //gcの定義
    auto c = reinterpret_cast<Complex*>(lua_touserdata(L, 1));
    std::destroy_at(c);
    return 0;
}

完成

最終的に完成したソースファイルがこちらです。

main.cpp
#include <lua.hpp>
#include <iostream>
#include <string>
#include <complex>

#define METATABLE_NAME "std::complex<double>"
#define complex_check(L, n) (reinterpret_cast<std::complex<double>*>(luaL_checkudata(L, n, METATABLE_NAME)))

typedef std::complex<double> Complex;

int new_complex(lua_State* L){   //複素数クラスの作成
   double r = lua_tonumber(L, 1); 
   double i = lua_tonumber(L, 2);
   
   // コンストラクタ
   Complex* p = lua_newuserdata(L, sizeof(Complex));
   auto c = new(p) Complex(r, i);

   //メタテーブルを設定
   luaL_getmetatable(L, METATABLE_NAME);  //メタテーブルをスタックに積む
   lua_setmetatable(L, -2); //メタテーブルをuserdataに設定

   return 1;
}

int complex_conj(lua_State* L){   //共役複素数を得る関数
   //引数を取得
   Complex* val = complex_check(L, 1);

   //共役を取得
   auto ret = std::conj(*val)
   
   //返り値用のクラス
   auto p = lua_newuserdata(L, sizeof(Complex));
   auto c = new(p) Complex(ret.real(), ret.imag());

   //メタテーブル設定
   luaL_getmetatable(L, METATABLE_NAME);
   lua_setmetatable(L, -2);

   return 1;
}

int complex__mul(lua_State* L){   //掛算を定義するメタメソッド
   //引数を取得
   Complex* val1 = complex_check(L, 1);
   Complex* val2 = complex_check(L, 2);
   
   //メモリ確保&メンバ初期化
   Complex* ret = reinterpret_cast<Complex*>( lua_newuserdata(L, sizeof(Complex)) );
   ret->real(0);   
   ret->imag(0);

   //メタテーブル設定
   luaL_getmetatable(L, METATABLE_NAME);
   lua_setmetatable(L, -2);

   //掛算を行う
   *ret = (*val1) * (*val2);
   return 1;
}

int complex__tostring(lua_State* L){   //tostringを定義するメタメソッド
   //引数を取得
   Complex* val = complex_check(L, 1);

   //文字列を作成しスタックへプッシュ
   std::string ret = "(" + std::to_string(val->real()) + "," + std::to_string(val->imag()) + ")";
   lua_pushstring(L, ret.c_str());

   return 1;
}

int complex__gc(lua_State* L){   //gcの定義
    auto c = reinterpret_cast<Complex*>(lua_touserdata(L, 1));
    std::destroy_at(c);
    return 0;
}


static luaL_Reg functions[] = {   //関数の登録
    { "new_complex", new_complex },
    { "conj", complex_conj },
    { nullptr, nullptr }
};

static luaL_Reg meta_functions[] = {   //メタテーブル用関数の登録
    { "conj", complex_conj },
    { "__mul", complex__mul },
    { "__tostring", complex__tostring },
    { "__gc", complex__gc },
    { nullptr, nullptr }
};

extern "C" {
    __declspec(dllexport) int luaopen_test_module(lua_State* L) {   //DLLの呼び出し
        //関数を登録
        luaL_register(L, "test_module", functions);

        //メタテーブルを作成
        luaL_newmetatable(L, METATABLE_NAME); 
        luaL_register(L, NULL, meta_functions);
        lua_pushstring(L, "__index");
		lua_newtable(L);
        luaL_register(L, NULL, meta_functions);
		lua_settable(L, -3);
        lua_pop(L, 1);
        
        return 1;
    }
}

これをAviUtlのスクリプト制御から呼び出します。

スクリプト制御
local t = require("test_module")

c1 = t.new_complex(1, 2) --複素数クラスの作成
debug_print( "type: " .. type(c1) )
debug_print( "c1: " .. tostring(c1) )

c2 = c1:conj() --共役複素数の取得
debug_print( "c2: " .. tostring(c2) )

c3 = c1 * c2 --複素数同士の掛算
debug_print( "c3: " .. tostring(c3) )
コンソール出力
type: userdata
c1: (1.000000,2.000000)
c2: (1.000000,-2.000000)
c3: (5.000000,0.000000)

さいごに

これで複素数クラスを作ることができました。
この方法を使えば、C++のクラスや構造体をLuaの変数として扱うことができます。
三次元ベクトルやファイルハンドル、色の構造体など多種多様な使い方ができるかと思いますので、是非活用してみてください。

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?