Visual Stduio 2019を使って、AviUtlで使えるDLLをつくる方法を備忘録として残します。
準備
Visual Studio 2019 は、あらかじめ用意しておきましょう。
ここからlua5_1_4_Win32_dll8_lib.zip
をダウンロードして、解凍しておきます。
それができたら、「新しいプロジェクトの作成」からC++の「空のプロジェクト」を選択します。
プロジェクトができたら、ソリューションエクスプローラーからソースファイルを右クリックして、cppファイルを作っておきます。
プロジェクトの設定
プロジェクト名を右クリックして、プロパティを開きます。
全般
詳細
C/C++
全般
追加のインクルードディレクトリ
に、先程ダウンロードしたLuaの中にあるinclude
フォルダを指定します。
リンカー
全般
追加のライブラリディレクトリ
にlua51.lib
のある場所を指定します。
入力
コーディング
今回はtest_module
という名前のプロジェクトだと仮定して、ソースコードを書いていきます。
以下が最低限のソースファイルです。
#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;
}
}
インクルード
インクルードは、lua.hpp
を指定します。
#include <lua.hpp>
関数
LuaとC++では、関数を直接呼び出したりすることはできません。
そのため、lua_State*
という引数を用いて、データをやり取りします。
int test_func(lua_Sate* L){
//ここにコード
return 0;
}
後述しますが、最後のreturn
には、Luaに渡す返り値の数を指定します。
引数と返り値のやり取り
前述したとおり、LuaとC++は直接関数を呼び出すことはできません。
そのため、スタックという概念を使用して、データをやり取りしています。
スタックについては、Wikipediaでも参照してください。
簡単に言うと、後入れ先出し__のデータ構造のことを言います。
スタックにデータを入れることを__プッシュ、スタックに積まれたデータを取り出すことを__ポップ__といいます。
ちなみに、スタックのインデックス(場所)は、下から1、2、3 または __上から-1、-2、-3__と数えます
以下のsum
関数を例にしてみます。
int sum(lua_Sate* L){
int num1 = lua_tonumber(L, 1); //スタックの一番下から値を取得
int num2 = lua_tonumber(L, 2); //スタックの下から2番目の値を取得
lua_pushinteger( L, num1+num2 ); //スタックに計算結果をプッシュ
return 1; //スタックからLuaに渡す返り値の数
}
このsum関数をAviUtl上で呼び出すと、
require("test_module")
local num1, num2 = 1, 2
local ans = test_module.sum(num1, num2) --sum関数を呼び出し
debug_print(ans) --ansを表示
結果は3
になります。
なにが起こっているのかと言うと、
① AviUtl(Lua)側からtest_module.dll
のsum
関数を呼び出し
② スタックに1と2が積まれる
③ スタックから値を取得し、test_module
内の変数に代入
④ num1とnum2を計算し、結果の値をスタックにプッシュ
⑤ スタックの1番上の値を、AviUtl(Lua)に返す
具体的には、
lua_to〇〇(lua_State*, int index)
: スタックのindex
番の値を取得
lua_push〇〇(lua_State*, value)
: スタックの一番上にvalue
をプッシュ
という関数で、これらのことを行っています。
最後のreturn 1;
は、Luaに渡す返り値の数を指定します。
(正確には、スタックから値を読み込むときに、上から何番目まで読み込むかを指定しています)
Luaから渡された引数の判別
渡された引数を判別するには、lua_type(lua_State* , int index)
関数を使います。
これは、スタックのindex
番にある値の型を返してくれる関数です。
返り値はint
ですが、define
されているので、以下の定数も使用できます。
Lua上での型 | lua_typeの返り値の型 |
---|---|
nil | LUA_TNIL |
number | LUA_TNUMBER |
boolean | LUA_TBOOLEAN |
string | LUA_TSTRING |
table | LUA_TTABLE |
function | LUA_TFUNCTION |
userdata | LUA_TUSERDATA |
thread | LUA_TTHREAD |
lightuserdata | LUA_TLIGHTUSERDATA |
以下が引数の型判別のサンプルコードです。
int num;
if (lua_type(L, 1) == LUA_TNUMBER) {
num = lua_tointeger(L, 1); //引数が数字だった場合
}
else {
return 0; //引数が数字じゃなかった場合
}
返り値をテーブルにする
DLLからLuaへ、テーブルで返り値を渡したいときには、少し特殊な処理が必要になります。
当然、std::vector
を直接返したりすることはできないので、スタックの操作が必要になります。
キーが数字のテーブル
まず、table[1]
のように、数値をインデックスとするテーブルについてですが、ここでは、lua_settable
関数を使用します。
lua_settable(lua_State* L, int index)
は、スタックの一番上の値をindex
にあるテーブルに代入します。
このときに使われるテーブルのインデックスは、スタックの上から2番目の値が使われます。
空のテーブルは、lua_newtable(lua_State* L)
で作成できます。
int getTable(lua_State* L) {
lua_newtable(L); //スタックに空のテーブルを積む
// table[1] = 10
lua_pushinteger(L, 1); //スタックにテーブルの番号を積む
lua_pushinteger(L, 10); //スタックに、テーブルへ入れたい値を積む
lua_settable(L, -3); //空のテーブルに値を入れる
// table[2] = 20
lua_pushinteger(L, 2);
lua_pushinteger(L, 20);
lua_settable(L, -3);
// table[5] = 50
lua_pushinteger(L, 5);
lua_pushinteger(L, 50);
lua_settable(L, -3);
return 1; //スタックの1番下(=テーブル)を返す
}
require("test_module")
local table = test_module.getTable() --テーブルを取得
debug_print( table[1] ) --10が表示される
debug_print( table[2] ) --20が表示される
debug_print( table[5] ) --50が表示される
このようにすると、CからLuaへテーブルを渡すことができます。
なお、lua_settable
を使用すると、使った値はスタック上から消滅します。
キーが文字列のテーブル
table["color"]
またはtable.color
のような、インデックスに文字列を使用したテーブルの場合は、lua_setfield
関数を使用します。
lua_setfield(lua_State* L, int index, const char* key)
は、スタックの一番上にある値をindex
にあるテーブルに代入します。
lua_settable
と似ていますが、キーには文字列key
しか使えません。
int getTable(lua_State* L) {
lua_newtable(L); //スタックに空のテーブルを積む
lua_pushinteger(L, 1); //スタックに、テーブルへ入れたい値を積む
lua_setfield(L, -2, "x"); //空のテーブルに値を入れる
lua_pushinteger(L, 2);
lua_setfield(L, -2, "y");
lua_pushstring(L, "0x000000");
lua_setfield(L, -2, "color");
return 1;
}
require("test_module")
local table = test_module.getTable() --テーブルを取得
debug_print( table["x"] ) --1が表示される
debug_print( table.y ) --2が表示される
debug_print( table["color"] ) --"0x000000"が表示される
後始末
static luaL_Reg functions[] = {
{"test_func", test_func}, //Luaから呼び出したい関数を記載
{ nullptr, nullptr } //リストの最後は nullptr にする
};
前半のstatic luaL_Reg functions[] = {...};
の部分は、LuaからDLLの関数を呼び出すときのリストです。
{"関数名", 関数}
というように指定して、リストの最後には{nullptr, nullptr}
を挿入します。
これをしないと、Luaから関数が認識されません。
extern "C" { //C言語で処理
__declspec(dllexport) int luaopen_test_module(lua_State* L) {
luaL_register(L, "test_module", functions); //関数リストを登録
return 1;
}
}
後半のextern "C" {...}
の部分は、Luaから呼び出せるように関数をエクスポートしています。
luaopen_〇〇
の部分や、luaL_register(L, "〇〇", functions);
の「〇〇」部分は、
自分のプロジェクトの名前に変更してください。
ビルド
最後に、ビルド
タブからソリューションをビルド
をクリックすると、ターゲットフォルダにDLLを含め、いろいろなファイルが生成されます。
この中の〇〇(プロジェクト名).dll
を、AviUtlのscript
フォルダにぶち込みましょう。
AviUtlで使う
AviUtlでDLLを使用するには、require
関数を使います。
DLLの関数を呼び出すには、「DLLの名前.関数()」という形にします。
require("test_module") --dll読み込み
test_module.function() --関数呼び出し
注意点として、DLLを一度読み込むと、AviUtlのキャッシュを削除しても再読み込みがされません。
DLLをビルドし直したら、AviUtlは再起動しましょう。
参考
Luaプログラミング入門 第6章 C言語との連携(Densanlabs)
AviUtlのスクリプト用にDLLつくろう(@SEED264)
C と lua の連携方法メモ(@miuk)
LuaのC++組み込み方自分用まとめ(@hiz_)
AviUtlスクリプト Wiki