なんだか雲行きの怪しい雑記帖

ぐだぐだ日記とメモと,あと不定期更新

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

【HSP】HSP用のプラグインHPIを作るまでの一通りの流れと寄り道


ずっと誰かがこういう記事書くだろうと思って早2,3年.
誰も意外とこういう記事書かないので書いてみようと思って書いてみます.


この記事は


HSP用のプラグイン(HPI)の作り方をSDKに沿って説明します.
基本的な作り方の説明が記事としての主眼ではありますが,ちょっとした小ネタみたいなのも混ぜていこうかなと思います.

というのも,私自身HSPのプラグインとしてちょっと規模が大きめ(だと自分では勝手に思っている)の,動的実行のためのプラグイン「mist」を作っている時にSDKで迷ったり悩んだりした点が「結構」あったので,探してもあまり資料なかったしどうせならそれらをまとめて資料として公開した方がいいかな,と思ったのが理由の一つです.
もう一つ個人的にはそこそこ大きな理由がありますが…そちらは後述.

HSP自体はバイトコードを埋め込んで実行させることができたり,COMを扱えるためプラグインなしでもかなり広範囲に手が届く(というよりは,やろうと思えばできる)ようになっていますが,それでもプラグインならではのメリットもあります.

ぱっと思いつくところでは
  • バイトコードを埋め込んだりせず,コマンド・関数を定義して機能追加できるため,無理せず拡張できる(機能自体の改変が比較的容易:ただし,モジュールよりは改変のし安さは難しいですが)
  • HSPのインタープリター上ではなくOS上で実行されるので高速
  • HSP内部の情報や変数情報も取得可能なため,通常のDLLに比べより状況に則した(HSP的に書きやすい)機能の追加が可能
  • ほぼ同一のコードでランタイム内にコードを組み込み,拡張ランタイムとして本体に埋め込み可能※

ら辺が分かりやすいメリットでしょう.
※ただしかなり難易度高いです,多分後述

対象読者

  • プラグイン作ってみたい人
  • HSPが書ける人
  • C/C++が若干書ける人
  • VisualStudioが触れる人



サンプルをビルドするまで


VisualStudioは必要なのでインストールしましょう,今だとVisualStudioCommunityエディションが拡張機能も使えて個人で使えるのでかなりお得ですね.(MSDN
なお,HSPSDK的には「Microsoft Visual C++(6.0以降)」とありますので新しめのバージョンでも作成は可能だと思われます.(ちなみに私がプラグイン作成に使っているのはVisualStudio2013です)

HPIのサンプルプロジェクトはHSPをインストールすると既に自動でついてきています.

HSPをインストールしたフォルダ内の「hspsdk」下「hpi3sample」にある「hpi3sample.vcproj」を開いてみましょう.
VisualStudioのバージョンによって変換ウィザードがでてきますが,そのまま進めれば開けます.

プロジェクトが開けたら,既にビルド可能な状態になっていますので,Ctrl+Shift+Bを押してビルドしてみましょう.
ビルドが完了すると,「hpi3sample」フォルダに更に「Debug」フォルダが作成されていて,その中に今ビルドした「hpi3sample.dll」が入っているはずです.
「hpi3sample」フォルダにある「hpi3sample.as」を「Debug」フォルダにコピーして実行してみても,無事エラーなく実行できることが確認できるかと思います.

ひとまず,プラグインをビルドすることが出来たので,次から具体的なプラグインのコードについて(初期化と,追加コマンドの定義方法など)見ていきましょう.


プラグインのコードの書き方:初級編


プラグインの初期化


まずプラグインの初期化です.
冒頭でHSP用のプラグイン形式の場合はHSPの内部情報にアクセスできることをメリットとして挙げましたが,HSPの内部情報にアクセスできるということは「プラグイン側のどこかしらで内部情報にアクセスできるように初期化している箇所がある」筈ですよね.
HSPでは,HSP用のプラグインは実行の一番最初に内部情報にアクセスできるよう,そして逆にHSP側からプラグインの情報にアクセスできるように初期化されます.

その初期化時に呼ばれる関数がhpi3sampleでは「hsp3cmdinit」にあたります,コードを見てみましょう.
EXPORT void WINAPI hsp3cmdinit( HSP3TYPEINFO *info )
{
// プラグイン初期化 (実行・終了処理を登録します)
//
hsp3sdk_init( info ); // SDKの初期化(最初に行なって下さい)
info->cmdfunc = cmdfunc; // 実行関数(cmdfunc)の登録
info->reffunc = reffunc; // 参照関数(reffunc)の登録
info->termfunc = termfunc; // 終了関数(termfunc)の登録
}


ここはほぼ定型句なのでそのままにしておいて問題ありません.
「hsp3sdk_init」によってプラグイン側からHSPの内部情報にアクセスできるようになります.
そして,その後の「cmdfunc」,「reffunc」,「termfunc」によってHSP本体からプラグインの関する情報が設定されます.

ちなみにこの関数ですが,HSP側からは一体どれが初期化用の関数なのか分からないので,「これが初期化の関数です」と明示的にHSPに示さなければなりません.
HSPのコード「hpi3sample.as」を覗いてみると,次のようなコードでHSP側に登録していることが分かります.
#regcmd "_hsp3cmdinit@4","hpi3sample.dll"


関数名の微妙な違いについて
C++のコード上では関数名は「hsp3cmdinit」ですが,実際にHSP側から登録している関数名は「_hsp3cmdinit@4」と,微妙に異なっています.
細かい説明は省略しますが,C++の関数名はコンパイラ・リンカから一意に求まるようにその呼出規約や関数シグネチャによって関数名が名前装飾されたものが用いられます.
今回の関数名の違いもそこに由来していますが,関数名を変えても名前装飾の規則は変わらないので,関数名を「functionmame」と変えた場合は「_functioname@4」として登録すれば正常に動作します.
これを元の関数名と一致させる方法はありますが,少々手間なのでここでは割愛します.


次はコマンド・関数実行のコア部分となる「cmdfunc」と「reffunc」について見ていきます.

プラグインによるコマンド実行


まずは簡単な方から,コマンド実行部分のコアである「cmdfunc」のコードは次.
static int cmdfunc( int cmd )
{
// 実行処理 (命令実行時に呼ばれます)
//
code_next(); // 次のコードを取得(最初に必ず必要です)

switch( cmd ) { // サブコマンドごとの分岐

case 0x00: // newcmd

p1 = code_getdi( 123 ); // 整数値を取得(デフォルト123)
stat = p1; // システム変数statに代入
break;

case 0x01: // newcmd2
newcmd2();
break;

case 0x02: // newcmd3
newcmd3();
break;

case 0x03: // newcmd4
newcmd4();
break;

default:
puterror( HSPERR_UNSUPPORTED_FUNCTION );// そんな命令知らない,という場合はエラーとする
}
return RUNMODE_RUN;
}


HSPには「このプラグインにはこういう(追加)命令があります」というのを予め登録しておくのですが,実際に登録した命令が「どのような引数をとるか」といった情報は保持していません,動的に変化するかも全てプラグイン次第ですからね.
かといって,命令があることだけ登録されてもHSPから呼び出す時に困るので,最低限HSPからプラグインへ命令処理を投げる際に連携がとれるよう,HSPでは「この命令がきたらこのID(数値)を使って通知してくれ」というような登録の仕方を採用しています.

「hpi3sample.as」の方では,
#cmd newcmd $000
#cmd newcmd2 $001
#cmd newcmd3 $002
#cmd newcmd4 $003

のようにしているところですね.
命令名と,更に数値を指定していることが分かります.

こうすることによって,例えば「newcmdという命令が実行される時は,プラグイン側にIDを$000で通知してくれ」という意味合いになります.
「newcmd2はIDを$001」,「newcmd3はIDを$002」,…と言った調子ですね.

さて,そう思ってプラグイン側のコードに戻って見てみると,どうやら引数として渡ってきた「cmd」がそのIDに該当している,ということがわかります.

プラグイン側でコマンド実行用の関数「cmdfunc」が呼ばれた際,するべきことは次です.
  • どの命令を実行しろという状況なのか,「cmd」から判断する
  • 各命令毎に必要な引数を取得する
  • 命令と引数から実際の処理を実行する


特にヒネた処理はないですね(強いていうなら「引数を取得する」処理を明示的にプラグイン側が書く必要性がある,ということぐらいですかね).

各命令で必要となる,引数の取得は取得したい引数の型によって次のようになります.

int型が欲しい場合

    // int型引数の取得:引数省略された場合はエラーとなる
int p1 = code_geti();
// デフォルト値つきint型引数の取得:引数省略された場合はデフォルト値が返ってくる
int p2 = code_getdi( 0 );


double型が欲しい場合

    // double型引数の取得:引数省略された場合はエラーとなる
double p1 = code_getd();
// デフォルト値つきdouble型引数の取得:引数省略された場合はデフォルト値が返ってくる
double p2 = code_getdd( 0 );


str型が欲しい場合

    // str型引数の取得:引数省略された場合はエラーとなる
char* p1 = code_gets();
// デフォルト値つきstr型引数の取得:引数省略された場合はデフォルト値が返ってくる
char* p2 = code_getds( "default" );


label型が欲しい場合

    // label型引数の取得:引数省略された場合はエラーとなる
unsigned short* p1 = code_getlb();


変数そのままが欲しい場合

    // 変数(var)引数の取得:引数省略された場合や変数でない場合はエラーとなる
PVal* pval;
APTR aptr;
aptr = code_getva( &pval );


ひとまずプリミティブな型と変数そのままの取得方法を列挙しました.
上述のように引数取得命令は「code_get○○」となっていて大変分かりやすいのですが…このうち,「str型の取得の仕方」は厳密には間違っています.
というのも,文字列型の引数はバッファサイズが大きくなることがあり,かついつメモリ領域を解放すればいいのか曖昧になりやすいため,次の引数取得のタイミングで文字列のバッファが解放されます.
そのため,文字列型の引数はなるべく早く自前のバッファにコピーしておくことが望ましいです.

str型が欲しい場合:(メモリ的に)正しいバージョン

char p1mem[256], p2mem[256];
// str型引数の取得:引数省略された場合はエラーとなる
char* p1 = code_gets();
strncpy( p1mem, p1, sizeof(p1mem) );// 取得した文字列をコピーしておく
// デフォルト値つきstr型引数の取得:引数省略された場合はデフォルト値が返ってくる
char* p2 = code_getds( "default" );
strncpy( p2mem, p2, sizeof(21mem) );// 取得した文字列をコピーしておく


面倒ですが割りと簡単にクラッシュするし大事なトコロです,文字列型の時は注意しましょう.

説明してないトコロについて
説明はしていませんが「cmdfunc」最初の「code_next」や,最後の「return RUNMODE_RUN」はほぼ定型句です.
必要ではありますが,まず改変しないため,ここでは説明を割愛しています.



プラグインによる関数実行


コマンドと関数,ほぼ同じですが一点だけ違いがあります.
それは,戻り値を返すこと.

ひとまずコードをみてみましょう,関数のコールバックのコードは次.

static int ref_ival;                        // 返値のための変数

static void *reffunc( int *type_res, int cmd )
{
// 関数・システム変数の実行処理 (値の参照時に呼ばれます)
//
// '('で始まるかを調べる
//
if ( *type != TYPE_MARK ) puterror( HSPERR_INVALID_FUNCPARAM );
if ( *val != '(' ) puterror( HSPERR_INVALID_FUNCPARAM );
code_next();


switch( cmd ) { // サブコマンドごとの分岐

case 0x00: // newcmd

p1 = code_geti(); // 整数値を取得(デフォルトなし)
ref_ival = p1 * 2; // 返値をivalに設定
break;

default:
puterror( HSPERR_UNSUPPORTED_FUNCTION );
}

// '('で終わるかを調べる
//
if ( *type != TYPE_MARK ) puterror( HSPERR_INVALID_FUNCPARAM );
if ( *val != ')' ) puterror( HSPERR_INVALID_FUNCPARAM );
code_next();

*type_res = HSPVAR_FLAG_INT; // 返値のタイプを整数に指定する
return (void *)&ref_ival;
}


ちょっと複雑になりましたね.
関数の冒頭と最後の方は定型文なので割愛しますが,注目すべきは戻り値の設定の仕方です.

必要になるのは「戻り値の型」と「戻り値の値(内容)」の2つで,「戻り値の型」は引数の「type_res」で,「戻り値の値(内容)」はそのポインタを関数の戻り値として返します.

上記SDK内のサンプルでは常にint型を返しているので「type_res」に「HSPVAR_FLAG_INT」を設定し,戻り値には「ref_ival」のポインタを返していますが,これがdouble型などの際は「type_res」に「HSPVAR_FLAG_DOUBLE」を設定し,戻り値にはdouble型のポインタを返せばよいことになります.

と書くと,『じゃあ文字列型の場合は「type_res」に「HSPVAR_FLAG_STR」を設定して,戻り値にはchar*型のポインタを返せば良いことになる』と思いそうですが,文字列型の場合返すポインタは「char*型」でOKです.

テストコードで書くと
*type_res = HSPVAR_FLAG_STR;
static const char* kRefstr = "refstr";
return reinterpret_cast<void*>( const_cast<char*>( kRefstr ) );

です,「char*」型であることに注意.

プラグインのデバッグの仕方

ちょっとだけ寄り道.

プラグインの開発中,バグとりが結構大変に思えることが多々あります.
なにせ,通常使えるVisualStudio上のブレークポイントが使えなかったりしますし…,それでいてC++はうっかりすると簡単にクラッシュするし….

ということで,ちょっと寄り道ですがプラグインのデバッグの仕方(VS上のデバッガに乗っける方法)を紹介します.
(DLL開発に慣れている人ならすぐに思いつくかと思うのですが,慣れてないと結構分からないものかなーと思います,ちなみに私はこのやり方になかなか気づきませんでした.)

まず,「hpi3sample」のソリューションエクスプローラから「hpi3sample」プロジェクトを右クリックし,プロパティを開きます.
そして,「構成プロパティ」から「デバッグ」を選び,「コマンド」に「<HSPをインストールしたディレクトリ>/hsp3.exe」を設定,「作業ディレクトリ」に「$(ProjectDir)/$(Configuration)」と設定します.

howtomakehpi_debug_prop.png


この状態でF5を押してデバッグ実行してみましょう.
「StartUp Failed」とダイアログが表示されれば,とりあえずはOKです,次のステップに進みましょう.

今どういう状況かというと,HSPのランタイム「hsp3.exe」を「hpi3sample.dll」があるディレクトリをカレントディレクトリとして動かしている状況です.
「hsp3.exe」は同じディレクトリ内にある「start.ax」があったらそれを実行するようなプログラムです.
この「start.ax」を実行させたいスクリプトから作成すれば無事デバッガ上でテスト用スクリプトを実行でき,かつプラグインの方でブレークポイントを貼れます.
「start.ax」の作成と言われるとピンとこないかもしれませんが,HSPスクリプトエディタからCtrl+F9で「実行ファイル自動作成」するときに生成されます.

つまり,実際の手順としては「Debug」ディレクトリに置いた「hpi3sample.as」をスクリプトエディタで開き,Ctrl+F9で「start.ax」を生成します.
ここでVisualStudioに戻り,もう一度F5を押してデバッグ実行させます.
無事実行できたら成功です,これでブレークポイントが貼れます.
アクセス違反などもこれで捕捉できるので,かなりデバッグが楽になるかと思います,プラグイン開発でバグに悩んでいる方は是非.

howtomakehpi_debug_break.png


プラグインのコードの書き方:中級編


閑話休題.

PValを知る


今まではHSPにコマンド・関数をプラグインから追加するだけのコード紹介でした.
しかし,プラグインを作る以上はもっと凝ったことをしたい! という人も多いかと思います…多いかと思ってます.
もっと凝ったことをするにはHSPの内部についての知識が必要になります,この節ではHSP内の変数を表す構造体であるPValについて説明します.

PValの中身


PValはHSPでいう変数の一つ一つに対応する構造体です.
変数の方の情報も含まれるため,ある変数のPValが取得できればどのような型であろうと追跡可能です.

さて,そんなPValの中身はこうなってます.
//    PVAL structure
//
typedef struct
{
// Memory Data structure (2.5 compatible)
//
short flag; // type of val:変数の型,HSPVAR_FLAG_INTなど
short mode; // mode (0=normal/1=clone/2=alloced):通常の変数,dupptrでクローンしている変数…など
int len[5]; // length of array 4byte align (dim):len[1]~len[4]が各次元の大きさ,その次元が確保されてない場合は0,なおlen[0]は値一つの大きさ
int size; // size of Val:変数が確保しているメモリのサイズ
char *pt; // ptr to array:変数が確保しているメモリのポインタ

// Memory Data structure (3.0 compatible)
//
void *master; // Master pointer for data
unsigned short support; // Support Flag:可変長配列や連想配列など,変数の型によってサポートされる機能を表すフラグ値
short arraycnt; // Array Set Count:内部で配列アクセス時に使われる一時変数
int offset; // Array Data Offsett:内部で配列アクセス時に使われる一時変数
int arraymul; // Array Multiple Value t:内部で配列アクセス時に使われる一時変数
} PVal;


HSPの内部理解に必要ではあるのですが,正直なところある程度のことなら追加で説明書いた「flag」と「len」だけ知っていれば大体大丈夫です.(特に「flag」に関しては変数の型を取得できるので極めて重要です)

さて,変数の情報がとれたらちょっと中身を表示して確認してみたいですよね.
初級編で説明した引数の取得でサラっと流して書いた「変数そのままの取得」ですが,取得した変数の情報を表示するコマンドを作ってみましょう.

新しくコマンドを作る場合,まず新しいコマンド用のIDを決めて,「cmdfunc」内の「switch」でそのコマンド用のIDを処理するようにします.

今回は適当に空いているID「0x04」を使ってみます.
static int cmdfunc( int cmd )
{
// 実行処理 (命令実行時に呼ばれます)
//
code_next(); // 次のコードを取得(最初に必ず必要です)

switch( cmd ) { // サブコマンドごとの分岐

case 0x00: // newcmd
~~省略~~

case 0x04:// 新規に作る命令
viewpval();// 命令の実際の処理はviewpval関数を作って,そっちに中に書きます
break;

default:
puterror( HSPERR_UNSUPPORTED_FUNCTION );
}
return RUNMODE_RUN;
}


そして,実際の変数の情報を表示する処理は次のような感じ.
static void viewpval()
{
// viewpval 命令の処理
//
// 引数として変数を一つとり,その情報をrefstrに格納
PVal *pval;
APTR aptr;
aptr = code_getva( &pval ); // 変数の取得(PValとAPTRポインタ)

// 内部で文字列バッファを作ってそこに書き込んでいく
char mem[ 1024 ];
sprintf(
mem,
"viewpval" "\r\n"
"flag : %d" "\r\n"
"len [0]=%d, [1]=%d, [2]=%d, [3]=%d, [4]=%d" "\r\n"
"size = %d" "\r\n"
"ptr = %d" "\r\n"
"aptr = %d",
pval->flag, pval->len[0], pval->len[1], pval->len[2], pval->len[3], pval->len[4], pval->size,
reinterpret_cast<int>( pval->pt ), aptr
);

// refstrにコピー
strncpy( ctx->refstr, mem, HSPCTX_REFSTR_MAX );
}

若干汚いですが…ご愛嬌

ではHSPから実際に呼び出して結果を表示してみましょう.
#regcmd "_hsp3cmdinit@4", "hpi3sample.dll"
#cmd viewpval $004

a = 3
viewpval a
mes ""+refstr


結果:
viewpval
flag : 4
len [0]=1, [1]=1, [2]=0, [3]=0, [4]=0
size = 4
ptr = 32595184
aptr = 0


渡す変数によって内容が(勿論)変わります,aをdouble型にすると次のような感じ.
結果:
viewpval
flag : 3
len [0]=1, [1]=1, [2]=0, [3]=0, [4]=0
size = 8
ptr = 31546608
aptr = 0


aがstr型だとこう.
結果:
viewpval
flag : 2
len [0]=1, [1]=1, [2]=0, [3]=0, [4]=0
size = 4
ptr = 31415536
aptr = 0


基本的にflagだけ変わっていって,その他は殆ど変動ないですね.
「code_getva」で返ってくるAPTR値は,変数の何番目の要素か(配列の添字)を表す値で,二次元配列以上の時でも要素を一意に定められます.

    ddim a, 16, 32
viewpval a( 3, 7 )
mes ""+refstr


結果:
viewpval
flag : 3
len [0]=1, [1]=16, [2]=32, [3]=0, [4]=0
size = 4096
ptr = 2875240
aptr = 115

APTRが結構大きい値になっていることが分かるかと思います.
基本的にこういった要素一つを扱う際はPValとAPTRはセットで扱うことになるので,覚えておくといいでしょう.


PValから値を取得・代入する


前回まででHSP中の変数一つに対応するPValとその要素を一意に指定できるAPTRについて説明しました.
この節ではもっと具体的に,要素に入っている値の取得や,代入を行ってみましょう.

とは言いつつ,HSPでは型の拡張も行えるため,先ほどのPValとAPTRを使って自分で値の取得・代入を行うのはかなり困難です.
そこでHSPではそれぞれの型に対応した取得や代入の処理を行ってくれる実体が存在します(ストレージコアファンクションと呼ばれま).

前節が結構流しめだったのはこいつのせいで,確かにPValの「flag」は型が特定できるので重要なのですが,実際にPValを介して値の操作を行う場合はストレージコアファンクションを介して行うため,PValの中身を直接変更することは極めて稀です.

では早速,例として変数引数をとり,その値を2倍した(同じ値を足した)もの変数に設定し直すコマンドを作ってみましょう.(また,取得した値の加工のサンプルとして,refstrに文字列として書き出す処理も入っています)
変数の操作はストレージコアファンクションを介して行います.

static void pvaltwice()
{
// pvaltwice 命令の処理
//
// 引数として変数を一つとり,その値をさらにもとの変数に足す
// 取得した値は確認のためrefstrに書き出します
PVal* pval;
APTR aptr;
aptr = code_getva( &pval ); // 変数の取得(PValとAPTRポインタ)

// ストレージコアファンクションの取得
HspVarProc* const scf = getproc( pval->flag );

// 値を取得
// ストレージコアファンクション介す場合,各変数の型に応じた値の大きさは可変な場合もあるため(文字列など),
// 値として取得できるのはその「値のポインタ」のみです
pval->offset = aptr;// 取得したい要素のAPTRをoffsetに設定
PDAT* ptr = scf->GetPtr( pval );// 値のポインタを取得

// 文字列として書き出し,refstrにコピー
char mem[1024];
switch( pval->flag )
{
case HSPVAR_FLAG_INT:
sprintf( mem, "%d", *reinterpret_cast<int*>( ptr ) );
break;
case HSPVAR_FLAG_DOUBLE:
sprintf( mem, "%lf", *reinterpret_cast<double*>( ptr ) );
break;
case HSPVAR_FLAG_STR:
strncpy( mem, reinterpret_cast<char*>( ptr ), sizeof(mem) );
break;
default:// あまりよく分からない型は処理しない方が無難
sprintf( mem, "型から実体を特定できませんでした" );
break;
}
strncpy( ctx->refstr, mem, HSPCTX_REFSTR_MAX );

// 同じ値を加算する:同じ型同士の場合は,値のポインタを渡すと処理してくれる
if ( pval->flag == HSPVAR_FLAG_STR )
{
// 文字列型だけちょっと都合が悪いので,別にコピーしたポインタを渡す
scf->AddI( ptr/* 足される方のPDAT */, mem/* 足す値のポインタ */ );
}
else
{
scf->AddI( ptr/* 足される方のPDAT */, ptr/* 足す値のポインタ */ );
}
}

static int cmdfunc( int cmd )
{
// 実行処理 (命令実行時に呼ばれます)
//
code_next(); // 次のコードを取得(最初に必ず必要です)

switch( cmd ) { // サブコマンドごとの分岐

case 0x00: // newcmd
~~省略~~

case 0x05:// また新たに作る命令
pvaltwice();
break;

default:
puterror( HSPERR_UNSUPPORTED_FUNCTION );
}
return RUNMODE_RUN;
}


HSPから呼ぶサンプルは次.
#regcmd "_hsp3cmdinit@4", "hpi3sample.dll"
#cmd pvaltwice $005

sdim a, 64, 2
a(2) = "str"
pvaltwice a(2)
mes "a:"+a(2)
mes "refstr:"+refstr

結果:
;a:strstr
refstr:str


このように値はストレージコアファンクションから得たポインタを介して扱うのですが,結局内部型に依存した処理になりやすかったりで大変です(特に文字列型とか).


添字からAPTRを計算する


上述のようにストレージコアファンクションでは要素を特定するためにAPTRが必要となるのですが,例えばHSP側から「この変数のこの範囲に対して処理したい」といった動作を書く場合,APTRの計算はプラグイン側ですることになるのですが,そういった際はどうするのかというと…

HspVarCoreReset( pval );// 配列ポインタをリセットする
exinfo->HspFunc_array( pval, d1 );// 1次元目
exinfo->HspFunc_array( pval, d2 );// 2次元目
exinfo->HspFunc_array( pval, d3 );// 3次元目
exinfo->HspFunc_array( pval, d4 );// 4次元目
const int kAptr = pval->offset;// offsetにAPTRが入っている

このようにするとHSP側が計算してくれます.
ただし,存在しない次元への添字指定は無効なので,そこは注意しないといけません(後述).
ちなみに,この時内部的に計算に使われるのが,PValの「arraycnt」とか「arraymul」だったりします.

ストレージコアファンクションを介さないで値を代入する


前節では主にストレージコアファンクションを介して値の取得・代入の仕方を説明しました.
HSP内部では型によらず統一的に値を扱うため,ポインタを介して処理している,というところがポイントでしたね.

しかし,プラグイン側から色々やっていると,わざわざストレージコアファンクションを通して設定するのは面倒な時があったりします.
実はHSPのSDKにはストレージコアファンクションを介さないで値を設定する機能があるので,ここではそれの説明をします.

その機能は「code_setva」,実は引数として変数を取得する「code_getva」に名前が似ている関数(マクロ)です.
使い方は簡単で,用意するのは代入したい変数のPValとAPTR,そして代入したい値のflag(HSPVAR_FLAG_**)とそのポインタです.
例えばint型の値を代入したいなら,
PVal* pval;
APTR aptr;
int value;
code_setva( pval, aptr, HSPVAR_FLAG_INT, &value );


という感じになります.コードが短くなっていいですね.


前小節「APTRの計算」と本小節で説明した内容について,簡単に動くサンプルコードとして,HSP側から引数で添字を渡し,そこに値を代入するようなコマンドを作ってみます.

static void pvalset()
{
// pvalset 命令の処理
//
// (pvalset var,value,d1,d2,d3,d4)
// 引数として変数と添字,そして代入したい値(int)をとる
// 取得した変数の添字要素に値を代入する
PVal* pval;
APTR aptr;
aptr = code_getva( &pval ); // 変数の取得(PValとAPTRポインタ):APTRは今回無視

// 代入したい値
int value = code_geti();

// 添字の取得(省略可)
int d1, d2, d3, d4;
d1 = code_getdi(0);
d2 = code_getdi(0);
d3 = code_getdi(0);
d4 = code_getdi(0);

// APTRの計算
HspVarCoreReset( pval );// 配列ポインタをリセットする
// 存在しない次元への添字指定はエラーとなるので分岐:汚い
if ( pval->len[1] > 0 )
{
exinfo->HspFunc_array( pval, d1 );// 1次元目
if ( pval->len[2] > 0 )
{
exinfo->HspFunc_array( pval, d2 );// 2次元目
if ( pval->len[3] )
{
exinfo->HspFunc_array( pval, d3 );// 3次元目
if ( pval->len[4] > 0 )
{
exinfo->HspFunc_array( pval, d4 );// 4次元目
}
}
}
}
int subst_aptr = pval->offset;

//
code_setva( pval, subst_aptr, HSPVAR_FLAG_INT, &value );
}

static int cmdfunc( int cmd )
{
// 実行処理 (命令実行時に呼ばれます)
//
code_next(); // 次のコードを取得(最初に必ず必要です)

switch( cmd ) { // サブコマンドごとの分岐

case 0x00: // newcmd
~~省略~~

case 0x06:// また新たに作る命令
pvalset();
break;

default:
puterror( HSPERR_UNSUPPORTED_FUNCTION );
}
return RUNMODE_RUN;
}


HSPから呼ぶサンプルは次.
#regcmd "_hsp3cmdinit@4", "hpi3sample.dll"
#cmd pvalset $006

dim a, 16
pvalset a(10), 9/* 代入したい値 */, 1/* 添字 */
mes "a:"+a(1)

結果:
a:9

第1引数として「a(10)」を指定しているのに,そのAPTRを無視して第3引数から得られる添字を採用しているので,代入されているのは「a(1)」であるところがポイントですね.
ちょっとずつプラグインでしかできないっぽいことしている感じがしてきました.

寄り道:引数取得再考,任意の型の引数を受け取る


今まではコマンド・関数で引数を取得する際に,「○番目の引数はint型だからcode_getdiで~~」という風にしていました.
でもよくよく考えると,折角のプラグインだし「○番目の引数がint型だったらこの処理,double型だったらこの処理」みたいな分岐もしてみたいものです.

PValの知識が必要となるので実は引数の取得の節では飛ばしていたのですが,ここまで来た方はもうPValもそんなに怖くないと思うので一旦引数の取得まで戻って説明したいと思います.

任意の型の引数を受け取る際は,「code_getprm」というのが使えます.
    // 任意の型の引数を受け取る
const int kChk = code_getprm();


ただしちょっと使い方が特殊で,「code_getprm」は任意の型以外にも,「そもそもその引数が省略されたのか」,あるいは「もう先には引数が存在しない」などの情報も取得できます.
「code_getprm」から返ってくる値は「今引数の状況はどうなっているか」を表しており,「正常に取得できた」,「引数省略された」,「もう引数ない」を知ることができます.

const int kChk = code_getprm();
if ( kChk <= PARAM_END )
{
// もう引数ない
}
else if ( kChk==PARAM_OK || kChk==PARAM_SPLIT )
{
// 引数ある!
}
else if ( kChk == PARAM_DEFAULT )
{
// 引数省略された
}

という感じで分岐で処理できます.

さて,引数ない場合と引数省略された場合は関係ないですが,引数ある場合はどうやってその引数の値を取得できるのでしょうか?
実はHSP側が定義しているグローバル変数でPVal*型の「mpval」というのがあります.

…お察しの方もおられるかと思いますが….
任意の型を受け取れるので内部的にどうなっていたかというと,現在の引数の内容を保持する変数が一個あり,それに対して処理をすることで任意の型に対する操作が可能になっているのでした.

なので,最初の方で説明した「int型とdouble型とで分岐する」ような処理を書きたい場合は,次のような感じになりますね.

const int kChk = code_getprm();
if ( kChk <= PARAM_END )
{
// もう引数ない
}
else if ( kChk==PARAM_OK || kChk==PARAM_SPLIT )
{
// 引数ある!
switch( mpval->flag )
{
case HSPVAR_FLAG_INT:
// int型の処理
break;
case HSPVAR_FLAG_DOUBLE:
// double型の処理
break;
default:
// ソレ以外…?
break;
}
}
else if ( kChk == PARAM_DEFAULT )
{
// 引数省略された
}


ただし,このmpvalは次の引数を取得すると当然のことながら前の引数の情報はなくなるので,逐次的に処理していく必要があります.(文字列型などはバッファが必要です)

プラグインのコードの書き方:番外編


私自身プラグインの上級者とかそういうのではないので,上級編とかそういうのはありません

もう前章までであらかたのプラグインは作れるようになった…と思います.
ここからは私が主にプラグインmist作っている時に使った(多分極めて邪悪な)小ネタを含む諸々の紹介となります,役に立たないものが多いと思いますがご承知を.
あと,説明とかしないでかなりコード貼り付けになるかと思いますがそちらもご了承願います.


内部で所持している関数(ユーザー定義,DLL)の取得


HSP内部ではユーザー定義関数やDLL内の関数を取得するコードです.
これぞプラグインならでは,に近いものです.(HSPスクリプトからも取得できるので,プラグイン限定の手法ではないのですが…)

static void viewInternalFuncs()
{
// 出力用バッファ
char mem[ HSPCTX_REFSTR_MAX ];
char tmp[ 256 ];
int bufSize = 0;

const auto add_buffer = [ &mem, &bufSize ]( const char* tmp )
{
sprintf( &mem[bufSize], "%s", tmp );
bufSize = strlen( mem );
};

// 内部で保持している関数の数
const auto kFuncNum = static_cast<int>( ctx->hsphed->max_finfo / sizeof(STRUCTDAT) );

// それぞれに対して…
for( int idx=0; idx<kFuncNum; ++idx )
{
// idx番目の関数の情報を取得
const auto& finfo = ctx->mem_finfo[idx];
int type = 0;
switch( finfo.index )
{
case STRUCTDAT_INDEX_FUNC:
case STRUCTDAT_INDEX_CFUNC:
// ユーザー定義関数
type = 1;
break;
case 0:
{
switch( finfo.subid )
{
case STRUCTPRM_SUBID_DLL:
case STRUCTPRM_SUBID_DLLINIT:
case STRUCTPRM_SUBID_OLDDLL:
case STRUCTPRM_SUBID_OLDDLLINIT:
// DLL関数
type = 2;
break;
}
break;
}
}

// 関数名の取得
const char* name ="<undefined name>";
if ( finfo.nameidx != 0 )
{
name =( &ctx->mem_mds[ finfo.nameidx ] );
}

// ここまで一旦出力
const char* const kTypeTable[] = { "不明", "ユーザー定義", "DLL" };
sprintf( tmp, "%s : %s : ", kTypeTable[type], name );
add_buffer( tmp );

// 引数の取得と出力
for( int i=0; i<finfo.prmmax; ++i )
{
const char* argType = "undefined";
const auto prm = reinterpret_cast<STRUCTPRM*>( &ctx->mem_minfo[ finfo.prmindex + i ] );
switch ( prm->mptype )
{
case MPTYPE_NONE: argType="none"; break;
case MPTYPE_VAR: argType="var"; break;
case MPTYPE_STRING: argType="str"; break;
case MPTYPE_DNUM: argType="double"; break;
case MPTYPE_INUM: argType="int"; break;
case MPTYPE_STRUCT: argType="struct"; break;
case MPTYPE_LABEL: argType="label"; break;
case MPTYPE_LOCALVAR: argType="local"; break;
case MPTYPE_ARRAYVAR: argType="array"; break;
case MPTYPE_SINGLEVAR: argType="var"; break;
case MPTYPE_FLOAT: argType="float"; break;
case MPTYPE_STRUCTTAG: argType="structtag"; break;
case MPTYPE_LOCALSTRING: argType="localstr"; break;
case MPTYPE_MODULEVAR: argType="modulevar"; break;
case MPTYPE_PPVAL: argType="ppval"; break;
case MPTYPE_PBMSCR: argType="pbmscr"; break;
case MPTYPE_PVARPTR: argType="varptr"; break;
case MPTYPE_IMODULEVAR: argType="imodulevar"; break;
case MPTYPE_IOBJECTVAR: argType="iobjvar"; break;
case MPTYPE_LOCALWSTR: argType="localwstr"; break;
case MPTYPE_FLEXSPTR: argType="flexsptr"; break;
case MPTYPE_FLEXWPTR: argType="flexwptr"; break;
case MPTYPE_PTR_REFSTR: argType="refstr"; break;
case MPTYPE_PTR_EXINFO: argType="exinfo"; break;
case MPTYPE_PTR_DPMINFO: argType="dpminfo"; break;
case MPTYPE_NULLPTR: argType="nullptr"; break;
case MPTYPE_TMODULEVAR: argType="tmodulevar"; break;
default: break;
}
add_buffer( argType );
add_buffer( "," );
}

// 改行
add_buffer( "\r\n" );
}

// refstrに出力
strncpy( ctx->refstr, mem, HSPCTX_REFSTR_MAX );
}

static int cmdfunc( int cmd )
{
// 実行処理 (命令実行時に呼ばれます)
//
code_next(); // 次のコードを取得(最初に必ず必要です)

switch( cmd ) { // サブコマンドごとの分岐

case 0x00: // newcmd
~~省略~~

case 0x07:
viewInternalFuncs();
break;

default:
puterror( HSPERR_UNSUPPORTED_FUNCTION );
}
return RUNMODE_RUN;
}

少しずつちゃんと書くのが面倒臭くなってきたのでC++11の書き方を少々入れつつ,記法がブレてるのはご勘弁を….

サンプルテスト用のHSPスクリプトは次.
#uselib "user32.dll"
#func global ActivateKeyboardLayout "ActivateKeyboardLayout" sptr,sptr

#regcmd "_hsp3cmdinit@4", "hpi3sample.dll"
#cmd viewinternalfuncs $007

viewinternalfuncs
mes ""+refstr
stop

#deffunc userfunc str, double, local, label
return
#defcfunc userfunc2 int, var, array
return

ActivateKeyboardLayout
userfunc
userfunc2

結果:

DLL : ActivateKeyboardLayout : flexsptr,flexsptr,
ユーザー定義 : userfunc : localstr,double,local,label,
ユーザー定義 : userfunc2 : int,var,array,


こんな感じでHSP内部のユーザー定義関数とかDLL関数とかが引っ張ってこれます.
書いてて思ったんですが思ったより引数の種類って多いんですね,知らなかったです.

動的にHSPの内部関数を呼び出す


mistの内部でやっていることです.
正直これが書きたくてこの記事を書きました,冒頭の個人的に書きたい理由がこれです.…にも関わらず,ここまでが長いですね…少し心が折れそうになりました

やっていることとしては,DLL側で「HSPの内部関数を呼び出す」バイトコード(HSP用)を生成し,それをHSPに実行してもらう,という流れになります.
そのため,内容としてはHSP用のバイトコード生成などが入ってきてしまうのですが,そこはここでは収まりきらない内容(かつ実は私はあんまり詳しくない)ため詳細は割愛します.

何を準備すればいいのか


簡単に『DLL側で「HSPの内部関数を呼び出す」バイトコード(HSP用)を生成し,それをHSPに実行してもらう』と書いていますが,そもそもそんなことできるようにHSPが設計されていません.
例えばHSPのバイトコード中で解釈される変数や値(リテラル)は予めコンパイルされデータ領域におかれ,バイトコード中にはそれらのIDが書き込まれた状態になっていたりします.

ただ,HSP内で持っているそれらのポインタを書き換えることだけはできます.
…というわけで,ざっくり仕組みを説明すると,

  • 呼び出したいコマンド・関数に渡す引数の実体(変数やリテラル)を自前で用意し,破綻のないHSP用のバイトコードを生成する
  • 生成した変数とリテラルを「ctx->mem_mds」と「ctx->mem_var」のそれぞれに上書きする(勿論,あとで復元できるように最初の値はとっておく).
  • 生成したバイトコードをHSPに実行してもらうため,バイトコードのポインタを「code_call」に渡して実行してもらう
  • 実行が終わった後は「ctx->mem_mds」と「ctx->mem_var」を書き換え前の状態に復元する


となります.
これだけ書くと簡単そうですが,実際はバイトコード生成の時点でかなり難しいです.


書きかけ
…サンプルコードは準備中…いつになるか分からないです;



プラグインを本体に組み込んで拡張ランタイムを作る


内容準備中…(やる気充電中)




あとがき


よくよく考えたらここに書いてあるうちの結構な情報が「hspsdk」内の「hspdll.txt」に書かれていた,ちゃんとリファレンスあったんだなぁ…あんまり見なかったけど.
多分リファレンスとしては貴重な資料だったんだろうけど,「結局どれとどれを組み合わせればできるの?」っていうのが分からなかったりしたから見なかったのかもしれない,でもそれは私だけなのかもかもしれない(みんな分かったからこそネット上には資料が殆ど見当たらなかったのかもしれない).

とかくそういうわけで,本記事では「実際に動くコード」に焦点をあててサンプルコード多めで紹介してみました.
どの言語も,どのモジュール(プラグイン)も,サンプル動かして動作掴むところはあると思いますし….
特にソフト作ってる側はデバッガがあるせいで「これであっているか分からないけどダメだったらブレークするだろ」論法でとりあえず走らせてバグ潰しするのは日常茶飯事…だと思ってるので,動くこって大事だしきっと価値ある…よね……?

ただ,この記事があらゆることを網羅しているかというと割りとそうでもなく,例えばHSPの内部バージョンをとるときは「exinfo->var」に入っているとか,そういうのは「hspsdk」内の「hspdll.txt」にかかれていたりするので,もっと色々知りたい人はそちらを参照願います.

たぶんこういう記事書いてもHSPのプラグイン増えないと思うけど,「もしかしたら」にちょっとだけ賭けて….


やる気がでたら続きをかく.


参考資料

先人の知恵とも言う。

MakeHPI:HSPWiki
Let's make plug-in for hsp3!!
HSP3の処理系の実装の要点メモ
関連記事
スポンサーサイト

コメント

コメントの投稿


管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://fe0km.blog.fc2.com/tb.php/118-e3ce666f
この記事にトラックバックする(FC2ブログユーザー)

FC2Ad

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。