概要
20年以上前に制作したC++言語のスタティックリンクライブラリをPythonから使うための追加手順です。
ライブラリは膨大な計算コストを必要とする処理で、しかも並列処理ができないという特性を持っています。
その技術は印刷技術のための古いものですが、なぜか近年のAIブームで再利用できるのでないかと再評価されている技術でもあります。
そこでAI向けに使われている代表的言語であるPythonで使えるように考えた次第です。
オープンソースとして公開することが目的で、すでにGitHubで公開しています。
個人的な、「PythoからのC++言語によるライブラリ呼び出し」、「ctypesパッケージ」、「生成AI利用開発の備忘録」を兼ねています。Windows版ですが、元のライブラリはOS非依存なのでLinux版にすることも簡単です。節の構成は以下の通り。
1.前提条件(1):動作確認済みのC++クラスライブラリが存在している
2.クラスライブラリをC言語インターフェースで動的リンクできるようにする
すでに動的リンクできるようになっている場合は飛ばす
3.Pythonのctypesパッケージを使って動的リンクライブラリAPIをPythonのクラスメソッドとして定義
4.Pythonでimportできるようにして動作確認
Pythonで使うことを想定していない場合は3.と4.は飛ばす
(1)C++クラスライブラリについて
ブルーノイズという乱数に似た数列を、非常に高速に生成するためのライブラリです。ブルーノイズは乱数を元に加工した数列なので乱数ではありません。もともとは256階調のグレイスケール画像を2~4階調で表示、フルカラー画像を3~4色のインクで表示するためのディザリング処理に使われていたものです。数列データ(別名ブルーノイズマトリクス)が、プリンタの制御部分やドライバなどに組み込まれていました。
ここ数年、ブルーノイズを乱数(C++のrand()やPythonのrandom())の代わりに使うことで、生成AIや強化学習に良い作用があるらしいことを示唆する研究があり、ディザリングだけでなく乱数発生器としても使えるようにしたライブラリを公開しようという目論見です。
参考文献:
勝田敏彦「でたらめの科学 サイコロから量子コンピュータまで」朝日新書
↑ブルーノイズ(bluenoise()やbluenoiseex())を使った相関のある擬似乱数発生器デモ
↑ホワイトノイズ(rand()やrandom())を使った乱数発生器デモ
Pythonで利用できる形での、C++製ライブラリ公開の手順、注意事項、つまづいた点を文章にしてみました。
(2)動的リンクライブラリの作成(Windows版)
VisualStudioでの操作を説明します。C++のクラスライブラリのソリューションに、動的リンクライブラリを作る新しいプロジェクトを追加します。やり方をすでに知っている方は、この節を飛ばしてください。"▶"ボタンをクリックすると内容が開きます。
VisualStudioによるdll(動的リンクライブラリ)作成
「新しいプロジェクトの作成」の選択肢には、ダイナミックリンクライブラリを作ることのできるテンプレートが二つあります。どちらでも目的は達成できるのですが、ここでは"MFC"が付く方(VisualStudioのバージョンにもよりますが「MFC DLL」や「MFCダイナミックリンクライブラリ」という選択肢)を選択します。 「構成マネージャー」で32bit/64bitの選択となりますが、Pythonに合わせます。本ライブラリは、32bit、64bitの両方で動作を確認しています。
オープンソースとして公開したときの構成が簡単になるように、一つのフォルダの下に全てのソースファイルとプロジェクトフォルダがあるようなフォルダ構成となっています。
VisualStudioが生成するテンプレートに、今後どうするかをコメントで加えたコードを下に示します
// bnmaskutf8dll.cpp : DLL の初期化ルーチンです。
// VisualStudioが生成したテンプレート
#include "stdafx.h"
#include "bnmaskutf8dll.h"
+// クラスライブラリのヘッダ
+#include "interbluenoise.h"
+// エラーコードのヘッダ
+#include "../errcode.h"
// ...............
// 中略
// ...............
// Cbnmaskutf8dllApp コンストラクション
Cbnmaskutf8dllApp::Cbnmaskutf8dllApp()
{
+ // ここでクラスライブラリのインスタンスを生成
+ // mpinterは"bnmaskutf8dll.h"で定義
+ mpinter = new CInterBluenoise();
+ if(mpinter) {
+ // インスタンスの初期化作業
+ mpinter->msetmatrixfolder(NULL,256);
+ mpinter->msetseed(0);
+ }
}
// VisualStudioが生成するテンプレートではディストラクタが無いので定義する
+Cbnmaskutf8dllApp::~Cbnmaskutf8dllApp()
+{
+ // コンストラクタで生成したクラスライブラリのインスタンスを破棄
+ if(mpinter) delete mpinter;
+}
// 唯一の Cbnmaskutf8dllApp オブジェクトです。
Cbnmaskutf8dllApp theApp;
// Cbnmaskutf8dl1App 初期化
BOOL Cbnmaskutf8dl1App::InitInstance()
{
CWinApp::InitInstance();
return TRUE;
}
// この後に動的リンクライブラリのAPIを定義していく
+/////////////////////
+// srandに対応するsbluenoise
+extern "C" __declspec( dllexport ) int __stdcall sbluenoise(unsigned int seed)
+{
+// MFC DLLでは、以下の1行は、各APIの先頭に必ず入れる。おまじないだと思っていれば良い。
+ AFX_MANAGE_STATE(AfxGetStaticModuleState());
+
+ if(theApp.mpinter) {
+ theApp.mpinter->msetseed(seed);
+ return(0);
+ }
+ return(INITIALIZE_ERROR);
+}
+
+/////////////////////
+// randに対応するbluenoise
+extern "C" __declspec( dllexport ) int __stdcall bluenoise()
+{
+ AFX_MANAGE_STATE(AfxGetStaticModuleState());
+
+ if(theApp.mpinter) {
+ return theApp.mpinter->mgetbluenoise();
+ }
+ return(INITIALIZE_ERROR);
+}
// 後略
C言語仕様のdllのAPI部分は、クラスのインスタンスを生成して各APIがクラスのインスタンスのメソッドを呼び出す構成にしてあります。
ほぼ機械的なコーディングなので生成AIの利用も可能ですが。APIはC言語であるのに対して他言語からの呼び出しのことを考慮しつつ設計する必要があるため手作業でコーディングしています。たとえば、
- 大きなバッファをアプリケーション側で確保して、そのアドレスをライブラリに渡すのか
- 大きなバッファをdll側で確保して、そのアドレスをアプリケーション側に返すのか
本稿のプログラムでは、開発上の理由からスタティックリンクライブラリと動的リンクライブラリの二つのビルドが必要となります。
特段の理由がなければ、クラスライブラリのソースコードを動的リンクライブラリのプロジェクトに入れてしまって、スタティックリンクライブラリは作らずに動的リンクライブラリだけをビルドしても構いません。
動的リンクライブラリの全ソースコードはGitHubの"bnmaskutf8dll.cpp"を参照してください。プロジェクトフォルダ"bnmaskutf8dll"にはC++言語から呼び出すための、コールバック関数宣言のヘッダファイル "bluenoisecallback.h" がありますが、Python、C#等の別言語から呼び出すときは不要なファイルです。
(3)Pythonのctypesパッケージを使って動的リンクライブラリのAPIをpythonクラスメソッドとして定義
VisualStudioの動的リンクライブラリのソースコードがあれば機械的な手順でできるので生成AIに任せてしまっても問題ありません。手作業では面倒でタイプミスも起きやすいので生成AIが大活躍です。数個くらいのAPIであれば、手作業でも効率は変わりませんが。20個を超えるようなAPIとなると生成AIに任せた方が効率は良くなります。
C言語インターフェースを持つwindowsの動的リンクライブラリ、"bnmaskutf8dll.dll"とpythonのインターフェースを取るためのpythonコードを作成しています。
dllのインターフェース部分のソースでの定義は以下のようになっています。
------- ここからdllのインターフェース部分 ------
extern "C" __declspec( dllexport ) int __stdcall sbluenoise(unsigned int seed)
略
pythonのインターフェース部分は、たとえば以下のようになります。
------- ここからpythonのインターフェース部分 ------
import ctypes
class CBluenoise:
def __init__(self):
self.dllhandle = ctypes.windll.LoadLibrary('bnmaskutf8dll.dll')
self.c_sbluenoise = self.dllhandle.sbluenoise
self.c_sbluenoise.restype = ctypes.c_int32
self.c_sbluenoise.argtypes = [ctypes.c_uint32]
def sbluenoise(self, seed):
self.c_sbluenoise(seed)
上記を参考に、以下のdllの全API関数に対応したpythonのインターフェース部分を作成してください。
"//" 以下のC++言語コメント部分はpythonでは"#"コメントに変更してください。
C言語の関数に対応したPythonのクラスメソッドもお願いします。
-------- dllの全関数 --------
extern "C" __declspec( dllexport ) int __stdcall sbluenoise(unsigned int seed)
(長いので略します)
(全APIの定義をそのまま。生成AIの入力トークンを減らすためコメントや関数の中身は省きます)
extern "C" __declspec( dllexport ) int __stdcall floydsteinberg4(int width,int height,int scanlinesize1,unsigned char* pdata1,unsigned char* pdata2)
Pythonで動作確認
動作確認は、CBluenoiseクラスのインスタンス"bn"を生成して、インスタンスメソッドを実行するだけ。画像は、Pythonのオブジェクト配列だと非効率なので、numpyのndarrayにすることでC++製のdllとの相性が良くなります。
画像処理(ディザリング)のテストをするには、何らかのライブラリ(PillowやOpenCV)で画像を読み込んで、必要の応じてndarray形式に変換してからnumpy.ndarray.tobytes()で画像データの入った単一のメモリブロックをC++のdllへ渡します。
画像処理済の画像が単一のメモリブロックで返るので、numpy.frombufferでndarrayに変換してから、reshapeで二次元画像に変換、matplotlibで画像表示して動作確認します。全ソースは、GitHub上の"bnmasktest.py"を参照。
bn = CBluenoise()
# 関数 bluenoise を実行
print('bluenoise API test')
for i in range(100):
ret = bn.bluenoise()
print('ret[%02d] = %d\t' % (i, ret), end="")
if(i % 5 == 4):
print('')
(4)Pythonでimportできるようにする
クラス定義の部分だけを抜粋した"bnmask.py"を"bnmask"というサブフォルダに入れて、中身の無い空のファイル"init.py"を"bnmask"サブフォルダ内に作ることで下記のようにクラスを"import"できるようになります。
GitHub上の"bnmaskimporttest.py"と"bnmask/bnmask.py"を参照。
from bnmask.bnmask import CBluenoise
# 以下、bnmasktest.pyと全く同じ
bn = CBluenoise()
# 関数 bluenoise を実行
print('bluenoise API test')
for i in range(100):
ret = bn.bluenoise()
print('ret[%02d] = %d\t' % (i, ret), end="")
if(i % 5 == 4):
print('')
下記の記事を参考にしました。