AviUtlではLuaを使用したスクリプティングが可能ですが、Luaの機能を使ってC++のクラスを変数として使用します。
準備
まずそもそも、LuaからC++の関数を使用するためにはDLLが必要です。
以前にこちらでも記述したので詳細は省きますが、以下のような手順をとります。
- Visual Studio 2019でC++のプロジェクトを作成する。
-
構成の種類
やターゲットファイルの拡張子
をdll
に設定。 - Lua5.1の中にある
lua51.lib
をライブラリとして指定する。またlua.hpp
のある場所をインクルードディレクトリに設定する。 -
文字セット
をマルチバイト文字セットを使用する
に設定する。 - C++のソースファイルを作成し、以下のようなコードを書く。
#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のソースコードを見れば一目瞭然です。
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)
を調べてみると、
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側でメモリの管理が行なえます。
#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
関数から取得できます。
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が呼び出された時点でメタテーブルをグローバルに登録しておきましょう。
#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用関数側でメタテーブルを複素数クラスに設定していきます。
#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
と設定して、以下のように変更してみました。
#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
をメタメソッドと呼びます(参考)。
#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
メソッドを持っていると、メモリ解放時に呼び出してくれます。ここでメモリを開放します。
int complex__gc(lua_State* L){ //gcの定義
auto c = reinterpret_cast<Complex*>(lua_touserdata(L, 1));
std::destroy_at(c);
return 0;
}
完成
最終的に完成したソースファイルがこちらです。
#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の変数として扱うことができます。
三次元ベクトルやファイルハンドル、色の構造体など多種多様な使い方ができるかと思いますので、是非活用してみてください。