はろーはろー、星野なたねだよ
解説するって言って1年近く経っているのでいい加減書くことにしたよ
今回はとりあえずVisual Studio 2017 Communityを使うよ
目次
準備
とりあえずVisual Studio 2017 Community(2017じゃなくてもいい)を入れてない場合はさくっと入れましょう。
C++を使うので当然C++をインストール項目から外すと話が始まりません。
また、Luaのバイナリ配布元からlua5_1_4_Win32_dll8_lib.zip
をダウンロードし、好きな場所に解凍しといてください。
プロジェクトの作成
New Project
からVisual C++
>Empty Project
で空のプロジェクトを作りましょう。
今回はAUL_DLL_Sample
で作りましたが、各自好きに決めてください。
プロジェクトができたら、設定の前にC++のソースファイルを作成しましょう(ソースファイルがない状態でプロパティを開いてもC++関連の設定はできないので)。
ファイル名は問いません。
諸々の設定
Solution Explorerでプロジェクトを右クリックし、プロパティを開きます。
左上のConfiguration(構成)でDebugとRelease、またはその両方に対して設定ができるので、最適化やデバッグに関する設定等、必要に応じて対象を切り替えながら設定していきましょう。
General(全般)
Output Directory(出力ディレクトリ)
でDLLの出力先を指定します。スクリプトのフォルダを指定しておくと、DLLに加えていろいろ生成されちゃうのはあるけどコピーの手間が省けていいと思います。
Target Name(ターゲット名)
は出力ファイルの拡張子抜きの名前です。プロジェクト名以外にしたい場合は変更しましょう。
Target Extension(ターゲットの拡張子)
は拡張子名です。.dll
に変更してください。
Configuration Type(構成の種類)
は実際にバイナリをどの形式にするかを指定します。Dynamic Library (.dll)
に変更してください。
C++
General(全般)
Additional Include Directories(追加のインクルード ディレクトリ)
は、#include
で読み込むヘッダのパスを指定します。今回はLua以外は特に使わないので先ほど導入したバイナリパッケージに含まれているincludeディレクトリを指定します。
Optimization(最適化)
Optimization(最適化)
で最適化の指定をします。目的にもよるけどOx
でいいんじゃないでしょうか。Ox
やO2
を選ぶ際は、後述のBasic Runtine Checks
を変更する必要があります。
最適化がかかると処理内容に変更が加わることもあるので、Debug構成では目的に応じて/Od
にして最適化を切っておく方が良いです。
Code Generation(コード生成)
Basic Runtime Checks(基本ランタイム チェック)
は、ランタイムのエラーチェックの指定です。内容としてはこんな感じですが、個人的にはDefaultでいいなと思います。最適化オプションと競合しがちだし。
ただ、Debug構成では/RTC1
あたりでチェックを入れておくとよさそうです。
Runtime Library(ランタイム ライブラリ)
でランタイムライブラリの指定をします。MTでいいんじゃないでしょうか。
Debug構成では、/MT
なら/MTd
、/MD
なら/MDd
が存在するのでそちらを選んでおきましょう。
Linker(リンカー)
General(全般)
Additional Library Directories(追加のライブラリ ディレクトリ)
で追加のライブラリのディレクトリを指定します。先ほどのパッケージのlua51.lib
があるディレクトリを指定します。
Input(入力)
Additional Dependencies(追加の依存ファイル)
で追加のライブラリを指定します。今回はlua51.lib
を指定します。
OpenCVなどのようにReleaseやDebugビルドでバイナリが違う場合は、構成毎に設定してください。
Debugging(デバッグ)
Generate Debug Info(デバッグ情報の生成)
でデバッグ情報を生成するか選びます。ReleaseのだけNoにしときゃいいんじゃないでしょうか。
コーディング
ではコードディングに入りましょう。
今回の想定として、Lua側でAUL_DLL_Sample.mult_color(data, w, h, red, green, blue)
と呼べる関数としましょう。
それぞれの引数で受け取った数を画像に乗算する単純なものです。
data,、w,、hはそれぞれobj.getpixeldata()
の返り値で受け取れる画像データと画像データの幅、高さです。
まずは
#include <lua.hpp>
で、Luaのヘッダ群をincludeします。これがないと始まりません。
次にLua側に登録できる形式の関数を作りましょう。
Lua側から呼べるCの関数は、lua_State
のポインタを受け取り、int
を返す形式です。とりあえずもっとも単純な形として、
int mult_color(lua_State *L) {
return 0;
}
としましょう。
引数の処理
Luaから呼び出す関数では、lua_State
を介して呼出し側からの引数を受け取ります。
Luaとの値のやり取りは基本的にスタックで行われます。今回受け取った引数(のRGBだけの場合)を簡単な図に表すとこのような感じで、積み木のように下から積みあがります。
まずは関数に最初に渡された画像データとサイズを受け取ってみましょう。
画像データのようなC側で扱うためのデータはUserdataと呼ばれ、lua_touserdata
関数でスタックから取り出すことができます
AUL側から受け取る画像データはピクセルのデータのw*h
個の一次元配列になっており、1pixelのデータはARGB各色1byteずつの計4byteとなっています。内部での配列は以下の図のように上位バイトから順にARGBの順に格納されているようです
一般的にはビットシフトで取り出すべきですが、面倒なので同じ内部配置の構造体を作ってそれの配列という解釈にします(かなり特殊な環境とかでバイトオーダー絡みの問題置きそうで怖いけど)
struct Pixel_RGBA {
unsigned char b;
unsigned char g;
unsigned char r;
unsigned char a;
};
画像サイズ等の整数値はlua_tointeger
関数で取り出せます。
それでは取り出しましょう。
Luaの関数は、第一引数にlua_State*
を受け取ります。
lua_to
系の関数は、第二引数にスタック上のインデックス値を受け取ります。
lua_touserdata
の返り値はvoid*
型なので、Pixel_RGBA*
にキャストしましょう。
Pixel_RGBA *pixels = reinterpret_cast<Pixel_RGBA*>(lua_touserdata(L, 1));
int w = static_cast<int>(lua_tointeger(L, 2));
int h = static_cast<int>(lua_tointeger(L, 3));
続いて、残りの3つの引数も受け取りましょう。
これらは整数ではなく浮動小数点数なので、lua_tonumber
で受け取ります。
返り値のlua_Number
はdouble
です。
double rm = lua_tonumber(L, 4);
double gm = lua_tonumber(L, 5);
double bm = lua_tonumber(L, 6);
画像処理
いよいよ画像データの処理に入ります。
画像データは左上を(0, 0)として、(0, y)~(w-1, y)の横方向のスライスがh個連続している形になっています。
特定の座標に対応する配列のインデックスはx + w * y
で計算できます。
新しいデータを作ってもいいのですが、今回は画像のサイズを変更することも特にないのでそのまま受け取ったデータを変更します。
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int index = x + w * y;
pixels[index].r *= rm;
pixels[index].g *= gm;
pixels[index].b *= bm;
}
}
xとyの二重でforループを行い、全ての座標のデータに操作を行います。
座標に依存する処理等を行う際は、一つのループで行うよりこちらの方がずっと簡単です。
座標に対応する配列要素の各色に受け取った係数を乗算しているだけです。
返り値について
Luaから呼び出す関数はintを返すことになっていますが、このintは実際にLua側に渡す返り値の数となっています。
CからLuaへは返り値もスタックを介して行われます。Lua側へ返したい値をスタックに積み、積んだ数を関数の返り値として返すことで、Lua側がスタックからその数だけ値を受け取るわけです。
今回は特に値は返さないので0を返します。
return 0;
関数のリストの作成
先ほど作った関数を、Luaに登録する関数のリストに追加しましょう。
リストはluaL_Reg
型の配列で、luaL_Reg
型の中身はconst char
配列の関数名と、lua_CFunction
型の関数ポインタです。
コーディングのはじめに述べた
Lua側から呼べるCの関数は、
lua_State
のポインタを受け取り、int
を返す形式です。
は、このlua_CFunction
の形で登録するためです。
内容はこのようになります。
static luaL_Reg functions[] = {
{ "mult_color", mult_color },
{ nullptr, nullptr }
};
リストの最後の要素は両方NULL
かnullptr
にしておいてください。
関数の登録とエクスポート
最後に、関数のリストをLuaからモジュールとして呼べるように登録する関数を作り、それをDLLから呼べるようにエクスポートします。
コードはこのようになります。
extern "C" {
__declspec(dllexport) int luaopen_AUL_DLL_Sample(lua_State *L) {
luaL_register(L, "AUL_DLL_Sample", functions);
return 1;
}
}
__declspec(dllexport)
を頭につけることによって、DLLの外部からでもluaopen_AUL_DLL_Sample
関数が見えるようにエクスポートします。
関数名は、luaopen_
に拡張子を除いたDLL名を合わせたものにしてください。詳しい規則はここを見るといいかもしれません。
関数はlua_State*
を引数に取り、int
を返します。
この関数内でluaL_register
関数を呼び出すことで、関数群をモジュールとして登録します。第二引数はモジュール名、第三引数は実際に登録するリストです。
複数のモジュールを登録したい場合は、その数だけluaL_register
を呼び出してください。
全体をextern "C"
で囲んでいる理由は、Lua側がluaopen_*
関数をDLLに埋め込まれるシンボル情報から探す際、Cの関数として宣言しないと見つけることができないからです。
Cの関数のシンボルは単純に関数名のみですが、C++は関数がオーバーロードできるという都合上、シンボルの衝突を避けるために関数名に引数や返り値の型などの情報を加えるマングリングという処理を行ったものをシンボルとして使うからです。詳しく知りたい人はここやここでも読んでみるといいんじゃないでしょうか。
スクリプト側の処理
DLLを作っても、呼び出すスクリプトがなければ使うことはできません。
DLLはスクリプトのファイルと同じディレクトリにあると仮定し、require
でDLLからモジュールを読み込みます。
require("AUL_DLL_Sample")
obj.getpixeldata
関数でDLL関数に渡すデータを受け取ります。引数は不要です。
data, w, h = obj.getpixeldata()
data
に画像データ、w
とh
に画像の幅、高さがそれぞれ入ります。
データの準備ができたら、引数を渡して関数を呼び出します。RGBの係数をそれぞれobj.track0~2
から受け取ると仮定して、
AUL_DLL_Sample.mult_color(data, w, h, obj.track0, obj.track1, obj.track2)
となります。
処理が終わったら、データをobj.putpixeldata
関数で反映させます。
obj.putpixeldata(data)
これでこのスクリプトを呼び出せば、晴れて自作DLLのエフェクトがかかることになります。
おわり
つかれた
解説クッッッッッッッッッッッッッッッッッッッッッソめんどくさかった
でもまぁまだいろいろ書けること自体はたくさんあるので、いつか気が向いたら書いてみようかなとは思います
気に入ったらついったで褒めてくれてもいいんだよ
多分かなり説明がへたくそで奇怪だったであろう文をここまで読んでくれてありがとうなのでした
次は期待しない程度に待っててね
あと今回のサンプルコードだよ
https://github.com/SEED264/AUL_DLL_Sample