Home -> HSP講座 -> HPI編 No.10-1

連想配列メイキング "assoc"

以前、型拡張プラグイン char を作成しましたが、今回はもう少し複雑な型を作ってみます。
たまり場で公開している var_assoc の簡易版的なものです。名前は同じく assoc ということで。

今回のプラグインのまとまったソースコードは、公開しているバージョンを参照してください。
多少、添字処理のまわりが食い違ってしまっていますが……。


hpimod

さて、実装を始める前に、使用するモジュール "hpimod" について少し説明をします。

このモジュールは、hpi の作成をサポートする各種関数をまとめたモジュールです。
「追加のインクルード先」に、hpimod と hspsdk を指定しておく必要があります。
あくまで、開発をする上で必要だったから作った、という代物なので、少々乱雑になっています。
+参照:hpimod の zip ファイル

※追記(2015/06/21)
最新版は GitHub で公開しています:「hpimod」リポジトリ
zip 版といろいろ違うので注意。

hsp3plugin_custom.h
hsp3plugin.h の代わりに読み込むヘッダです。
たまに使うかもしれないし使わないかもしれない、いろいろが追加されています。
makepval
変数の管理をする構造体 PVal* を操作する関数を集めたモジュールです。
ほとんど、hspvar_core.cpp の中にある関数のコードを参考に作っています。
argGetter
コードを処理する関数がいくつかまとめてあるモジュールです。
関数の引数、配列の添字を処理するのに使えますが、今回の用途は主に後者です。
func_result
関数コマンド (reffunc) の返値を設定するのを楽にするモジュールです。
これまではいちいち ref_ival などのグローバル変数を用意していましたが、テンプレートとオーバーロードでそのあたりを自動化しています。
試作品なのでだいぶテキトーだったり……。(特に str 型)

型定義

assoc 型の各要素は、次の CAssoc というクラス (へのポインタ) で管理します。

#1 CAssoc インターフェース

class CAssoc
{
public:
	typedef std::string Key_t;			// キーの型
	typedef std::map<Key_t, PVal*>  Map_t;		// 連想配列
	typedef std::pair<Key_t, PVal*> MapPair_t;	// 連想配列のペア
	
public:
	PVal* At( const Key_t& key );	// 内部変数取得
	// 他
	
	static CAssoc* New();	// 生成
	void AddRef();		// 参照カウンタ +1
	void Release();		// 参照カウンタ -1
	
private:
	CAssoc();
	~CAssoc();
	
private:
	Map_t* mpMap;
	int mCnt;		// 参照カウンタ
};

このクラスは CAssoc の実体ではなく CAssoc* の形で使います。
そのため、コンストラクタは private にして隠蔽し、生成にはクラス関数 New() を用います
(New の内部に new CAssoc という式が存在する)。

CAssoc のメンバ関数は、インターフェースの方が重要なので、実装は後回しです。
参照カウンタ」(メンバ mCnt, AddRef, Release に関係する) についても、後で説明します。

#2 Alloc, Free — 確保, 解放

HSPで型の振る舞いを定義するのは、HspVarProc 構造体です。
今回もやはり Alloc, Free を先に定義します。

//------------------------------------------------
// PValの変数メモリを確保する
//
// @ pval は未確保 or 解放済みの状態。
// @ pval2 != NULL なら pval2の内容を継承する。
//------------------------------------------------
static void HspVarAssoc_Alloc( PVal* pval, const PVal* pval2 )
{
	if ( pval->len[1] < 1 ) pval->len[1] = 1;	// 配列を最低1は確保する
	size_t cntElems = PVal_cntElems( pval );	// 全要素数 (hpimod/mod_makepval)
	size_t     size = cntElems * sizeof(CAssoc*);
	
	// バッファ確保
	CAssoc** pt = (CAssoc**)hspmalloc( size );
	memset( (char*)pt, 0, size );			// NULLクリア
	
	// 継承
	if ( pval2 != NULL ) {
		memcpy( (char*)pt, pval2->pt, pval2->size );	// 持っていたデータをコピー
		hspfree( pval2->pt );				// 元のバッファを解放
	}
	
	// 初期化
	for ( int i = 0; i < cntElems; ++ i ) {
		if ( pt[i] == NULL ) {
			pt[i] = CAlloc::New();	// 新しいオブジェクトの生成
			pt[i]->AddRef();	// 所有
		}
	}
	
	// pval へ設定
	pval->flag   = g_vtAssoc;	// assoc の型タイプ値
	pval->mode   = HSPVAR_MODE_MALLOC;
	pval->size   = size;
	pval->pt     = (char*)pt;
	pval->master = NULL;		// 後で使う
	return;
}

//------------------------------------------------
// PValの変数メモリを解放する
//------------------------------------------------
static void HspVarAssoc_Free( PVal* pval )
{
	if ( pval->mode == HSPVAR_MODE_MALLOC ) {
		// 全ての要素を Release
		CAssoc** pt = (CAssoc**)pval->pt;
		size_t cntElems = PVal_cntElems( pval );
		
		for ( size_t i = 0; i < cntElems; ++ i ) {
			if ( pt[i] ) {
				pt[i]->Release();	// 所有権を捨てる
				pt[i] = NULL;
			}
		}
		
		// バッファを解放
		hspfree( pval->pt );
	}
	
	pval->pt   = NULL;
	pval->mode = HSPVAR_MODE_NONE;
	return;
}

Alloc で AddRef 、Free で Release を行う以外は、前と似たようなものですね。
assoc 型は、値として (CAssoc*)nullptr も取れる仕様なのですが、初期化では実体を確保することにします。

#3 Set — 代入

次に、演算関数です。
今回は「代入」だけとします。

//------------------------------------------------
// 代入 (=)
// 
// @ 参照共有
//------------------------------------------------
static void HspVarAssoc_Set( PVal* pval, PDAT* pdat, const void* in )
{
	CAssoc*& dst = *((CAssoc**)pdat);
	CAssoc*& src = *((CAssoc**)in);
	
	if ( dst != src ) {
		if ( dst ) dst->Release();
		dst = src;
		if ( dst ) dst->AddRef();
	}
	
	g_pHvpAssoc->aftertype = g_vtAssoc;
	return;
}

連想配列を複製する (CAssoc をもう一つ作り、すべてのキーと値のペアをそれにコピーする) のは重すぎるので、左右のポインタを同じ値にして、同じオブジェクトを共有することにします。
すると、左右両方の変数がこのオブジェクトを手放すまで、このオブジェクトは破棄できなくなります。どちらか片方だけの判断でオブジェクトを破棄してしまうと、他方が使っているオブジェクトが突然失われてしまうことになるからです。

ここで、オブジェクトを安全に破棄するための仕組みとして、「参照カウンタ」が登場します。

void CAssoc::AddRef()
{
	mCnt ++;
	return;
}

void CAssoc::Release()
{
	mCnt --;
	if ( mCnt == 0 ) Delete(this);		// 破棄
	return;
}

ここでは mCnt が「参照カウンタ」で、この数値は、そのオブジェクトを“所有する”変数の個数を表しています。
参照カウンタが 0 になったときに、そのオブジェクトは誰にも所有されていない=不必要になった、と判断して、破棄します。

※ちなみに、「循環参照」という状況になると、もう不要なのに参照カウンタが 0 にならない、という現象が起こり、問題になります。
 ……が、今回は気にしません。

#4 他

他の関数の多くは、char 型が CAssoc* 型に置き換わっただけで、すること同じです。

//------------------------------------------------
// Core
//------------------------------------------------
static PDAT* HspVarAssoc_GetPtr( PVal* pval )
{
	return (PDAT*)( (CAssoc**)pval->pt + pval->offset );
}

//------------------------------------------------
// Size
//------------------------------------------------
static int HspVarAssoc_GetSize( const PDAT* pdat )
{
	return sizeof(CAssoc*);
}

//------------------------------------------------
// Using
//------------------------------------------------
static int HspVarAssoc_GetUsing( const PDAT* pdat )
{
	return (int)(*(CAssoc**)pdat != NULL);
}

//------------------------------------------------
// ブロックメモリ
//------------------------------------------------
static void* HspVarAssoc_GetBlockSize( PVal* pval, PDAT* pdat, int* size )
{
	*size = pval->size - ( ((char*)pdat) - ((char*)pval->pt) );
	return pdat;
}

static void HspVarAssoc_AllocBlock( PVal* pval, PDAT* pdat, int size )
{
	return;
}

//------------------------------------------------
// Assoc 登録関数
//------------------------------------------------
void HspVarAssoc_Init( HspVarProc* p )
{
	g_pHvpAssoc     = p;
	g_vtAssoc       = p->flag;
	
	// 関数ポインタを登録
	p->GetPtr       = HspVarAssoc_GetPtr;
	p->GetSize      = HspVarAssoc_GetSize;
	p->GetUsing     = HspVarAssoc_GetUsing;
	
	p->Alloc        = HspVarAssoc_Alloc;
	p->Free         = HspVarAssoc_Free;
	p->GetBlockSize = HspVarAssoc_GetBlockSize;
	p->AllocBlock   = HspVarAssoc_AllocBlock;
	
	// 演算関数
	p->Set          = HspVarAssoc_Set;
	
	// 連想配列用
	p->ArrayObjectRead = HspVarAssoc_ArrayObjectRead;	// 参照(右)
	p->ArrayObject     = HspVarAssoc_ArrayObject;		// 参照(左)
	p->ObjectWrite     = HspVarAssoc_ObjectWrite;		// 格納
	p->ObjectMethod    = HspVarAssoc_ObjectMethod;		// メソッド
	
	// その他設定
	p->vartype_name = "assoc";		// タイプ名
	p->version      = 0x001;		// runtime ver(0x100 = 1.0)
	
	p->support				// サポート状況フラグ(HSPVAR_SUPPORT_*)
		= HSPVAR_SUPPORT_STORAGE	// 固定長ストレージ
		| HSPVAR_SUPPORT_FLEXARRAY	// 可変長配列
		| HSPVAR_SUPPORT_ARRAYOBJ	// 連想配列サポート (後述)
		| HSPVAR_SUPPORT_NOCONVERT	// ObjectWriteで格納 (後述)
		| HSPVAR_SUPPORT_VARUSE		// varuse関数を適用
		;
	p->basesize = sizeof(CAssoc*);		// size / 要素 (byte)
	return;
}

#5 ArrayObject, ArrayObjectRead

「連想配列」(associative array)とは、簡単にいえば「添字が(0以上の)整数じゃなくてもいい配列」のことです。
たくさん別名があって、「マップ」(map)「辞書」(dictionary)とも呼ばれます。

HSPでも、「連想配列型」は上記の意味で使われていますが、実際には「添字をプラグインが処理する型」と呼ぶべきものです。
一般的な (int などの型の) 配列変数の添字は、「4つまでの整数を受け取り、pval->offset の値を設定する」という方法で処理され、これはHSPランタイムが行います。

// 連想配列ではない配列
	dim a, 4, 3, 2, 1
	a(3, 2, 1, 0) = 3210
	mes a(3, 2, 1, 0)	// 最大4つの整数で要素を指定する、という使い方

一方、連想配列型(comobj, assoc など)では、配列変数に与えられた添字を処理するために、プラグインが持つ関数を呼び出します。
それが HspVarProc::ArrayObject, HspVarProc::ArrayObjectRead です。前者は変数が左辺値として参照されるとき、後者は右辺値として参照されるときに、呼ばれます。

2つの関数の使い分けは、C/C++にある「左辺値」「右辺値」という言葉のイメージから分かると思いますが、一応HSPの場合について説明しておきます。
「(変数が)左辺値として参照される」とは、変数が「代入文の左辺」か「変数を受け取る引数」に書かれていることです。
それ以外の場合、変数は「右辺値として参照される」といいます。

// HSP における左辺値、右辺値の出現例

	// ・配列変数を受け取る引数に書かれている場合
	// ( dim の第一引数は配列変数を受け取る )
	dim a, 4
	
	// ・変数要素を受け取る引数に書かれている場合
	// ( memcpy は第一、第二引数に変数を受け取る )
	memcpy x, a(1), 4
	
	// ・代入文の左辺にある場合
	// ※ += などの複合代入も含む
	a(1)  = 7
	a(1) += 1

※#deffunc の array, var 引数を例に出した方が分かりやすいかも?

// ArrayObject, ArrayObjectRead が呼ばれる場面の例
// a を連想配列型の変数とする

	a("index") = ...
	// ↑ a の型の HspVarProc::ArrayObject が呼ばれる
	// "index" というのは ArrayObject の中で code_get() で取り出され、
	// どう使われるかはプラグイン次第
	
	mes a("index")
	// ↑ a の型の HspVarProc::ArrayObjectRead が呼ばれる 

まずは前者、ArrayObject からです。

//------------------------------------------------
// 連想配列::参照 (左辺値)
// 
// @ ( idx...(0〜4個), "キー", 内部変数の添字... )
//------------------------------------------------
static void ArrayObject( PVal* pval )
{
	if ( *type == TYPE_MARK && *val == ')' ) return;	// 添字状態を更新しない
	
	bool bKey = false;		// キーがあったか
	
	// [1] assoc 自体の添字と、キーを取り出す
	...
	
	// [2] 参照先 (assoc or 内部変数) を決定
	...
	
	// [3] 内部変数の添字を取り出す
	...
	
	return;
}

大雑把な流れはこの通りです。

ArrayObject, ArrayObjectRead ともに、添字の '(' を取り出した直後に呼ばれます
もし、この関数が呼ばれた時点で次の字句 (type, val) が ')' だったら、それは

	a()

という状態になっていることを表しています。
assoc 型ではこれを「a の直前の添字と同じものを指し示す」ことにしているので、ここでは何も変更することなく関数の処理を終えています。
これを認めずにエラーを出す仕様でもいいかもしれませんね。

コメントに書きましたが、assoc 型の添字の仕様は、最初の int 4つが assoc 要素の決定、次の引数がキー文字列、それ以降の引数が内部変数の添字です (var_assoc と同じ仕様)。
assoc 型も int などと同じように、配列となることができます。つまり、内部に CAssoc* が複数個存在するので、そのどれを参照するかを最初の4つの整数で決定します。
文字列型の要素が来たら、それはキーとして取得します。キーを取得した時点で内部変数が決定するので、それ以降の要素はその内部変数の添字とします。

では、順番に一部分ずつ実装しましょう。
配列の添字は、関数の引数を取り出すのと全く同じ方法で取り出します。

	// [1] assoc 自体の添字と、キーを取り出す
	
	HspVarCoreReset( pval );	// 添字設定の初期化
	for ( int i = 0; i < 5 && code_isNextArg(); ++ i )
	{
		PVal pvalTemp;
		HspVarCoreCopyArrayInfo( &pvalTemp, pval );	// 添字状態を保存
		int chk = code_getprm();
		HspVarCoreCopyArrayInfo( pval, &pvalTemp );	// 添字状態を復帰
		
		if ( chk == PARAM_DEFAULT ) puterror( HSPERR_NO_DEFAULT );
		if ( chk <= PARAM_END ) break;
		
		// int (最大4連続)
		if ( mpval->flag == HSPVAR_FLAG_INT ) {
			if ( pval->len[i + 1] <= 0 || i == 4 ) puterror( HSPERR_ARRAY_OVERFLOW );
			
			code_index_int_lhs( pval, *(int*)mpval->pt );	// 配列要素指定 (左辺値)
			
		// str (キー)
		} else if ( mpval->flag == HSPVAR_FLAG_STR ) {
			bKey = true;
			break;
		}
	}
	
//	[続]

この処理は、int 型などの添字の処理と同じです ( hsp3code.cpp の code_checkarray() 参照 )。
4次元配列になっている場合は 、キーの分と合わせて5回ループするため、条件が i < 5 になっていることに注意です。
また、code_index_int_lhs() は hpimod の mod_argGetter の関数で、exinfo->HspFunc_array の、自動拡張に対応したバージョンです。詳しくはそちらを参照してください。

キーを取り出したとき、bKey を true とします。
この部分が終了した時点で、bKey が true なら mpval はキーの値を持ちます。

	// [2] 参照先 (assoc or 内部変数) を決定
	
	if ( !bKey ) {	// キーなし => assoc 自体への参照
		pval->master = NULL;
		return;
	}
	
	CAssoc** pAssoc = (CAssoc**)HspVarAssoc_GetPtr(pval);
	if ( *pAssoc == NULL ) {		// 自動的に実体化
		(*pAssoc) = CAssoc::New();
		(*pAssoc)->AddRef();
	}
	
	PVal* pvInner = pAssoc->At( (char*)mpval->pt );	// 内部変数
	
	pval->master = pvInner;
	
//	[続]

NULL状態のオブジェクトに参照すると、普通はNULL参照エラー(いわゆる「ぬるぽ」)を引き起こすのですが、いちいちチェックしてもらうのも面倒なので、NULL参照時は自動的にオブジェクトを実体化することにします。
おせっかい仕様でいきましょう。

ここで、pval->master を使っています。このメンバはHSPランタイムでは使われていない、いわば余り物です。
assoc 型ではこれを、添字の「参照先」を表すポインタとして利用します。
つまり PVal::master は内部変数へのポインタ (PVal*) か nullptr を持ち、nullptr なら、assoc 自体が参照されていることを意味する、ということにします。

	// [3] 内部変数の添字を取り出す
	
	if ( code_isNextArg() ) {			// 添字が続く場合
		code_expand_index_impl( pvInner );
	} else {
		HspVarCoreReset( pvInner );		// 添字状態の初期化だけしておく
	}
	
	return;

code_expand_index_impl というのは、hpimod の mod_argGetter に含まれる関数で、次のようになっています。

//------------------------------------------------
// 配列要素の取り出し (中身だけ)
// 
// @ '(' の取り出しは終了しているとする
//------------------------------------------------
void code_expand_index_impl( PVal* pval )
{
	// 連想配列型 => ArrayObject() を呼ぶ
	if ( pval->support & HSPVAR_SUPPORT_ARRAYOBJ ) {
		( exinfo->HspFunc_getproc(pval->flag) )->ArrayObject( pval );
		
	// 通常配列型 => 次元の数だけ要素を取り出す
	} else {
		PVal pvalTemp;
		HspVarCoreReset( pval );
		
		for ( int i = 0; i < ArrayDimMax && !(*type == TYPE_MARK && *val == ')'); ++ i ) {
			HspVarCoreCopyArrayInfo( &pvalTemp, pval );
			int idx = code_geti();
			HspVarCoreCopyArrayInfo( pval, &pvalTemp );
			
			code_index_int_lhs( pval, idx );
		}
	}
	
	return;
}

※ hpimod/mod_argGetter.cpp から引用。

左辺値として参照される配列の添字を処理する関数です。

続いて ArrayObjectRead です。
これは、連想配列から右辺値、つまり実際に値データを取り出すために呼び出されます。
そのため「添字処理」の後に「値データを具体的に求める」という工程が追加で必要です。

添字処理は、基本的に ArrayObject と同じなので、ArrayObject そのものを呼び出して済ませます。
本当は、ArrayObjectRead のときは自動拡張を行わない方がいいのですが。

//------------------------------------------------
// 連想配列::参照 (右辺値)
//------------------------------------------------
static void* ArrayObjectRead( PVal* pval, int* mptype )
{
	ArrayObject( pval );	// 添字処理
	return ArrayObjectReadImpl( pval, mptype );
}

そして参照先の値を返すのですが、それを ArrayObjectReadImpl という関数に分割しました。
ここで、もし内部変数も assoc 型ならば、それを ArrayObjectRead() で再帰的に処理することにします。

static void* ArrayObjectReadImpl( PVal* pval, int* mptype )
{
//	assert( pval->flag == g_vtAssoc );
	
	PVal* pvInner = (PVal*)pval->master;	// 内部変数
	
	// assoc 自体の参照
	if ( pvInner == NULL ) {
		*mptype = g_vtAssoc;
		return (CAssoc**)HspVarAssoc_GetPtr(pval);
		
	// 内部変数 [assoc] => 再帰呼び出し
	} else if ( pvInner->flag == g_vtAssoc ) {
		return ArrayObjectReadImpl( pvInner, mptype );
		
	// 内部変数 [others]
	} else {
		*mptype = pvInner->flag;
		HspVarProc* pHvp = exinfo->HspFunc_getproc( pvInner->flag );
		return pHvp->GetPtr( pvInner );
	}
}

この再帰的処理は、次のように入れ子の連想配列から値を取り出すときに用いられます。

	// t を連想配列型にする
	assoc t
	
	// t の内部変数 t("root") を連想配列型にする
	assocDim t("root"), assoc
	
	t("root", "nodeA") = "leafA"
	mes t("root", "node")	//←ここ

内部変数の添字も実は再帰的に処理されていることにお気づきでしたか?
内部変数が連想配列型なら、ArrayObject の「内部変数の添字を取り出す」部分で、再び ArrayObject が呼び出されるのです。

では、「再帰的取り出し」の前にある代入処理を、次に実装しましょう。

#5 ObjectWrite — 配列要素への単純代入

HSPVAR_SUPPORT_NOCONVERT (格納処理) をサポートしておくと、代入文の処理もある程度、独自にやらせてもらえます。
というか、添字の処理で特殊な操作(pval->masterを使ったところのこと)を行った以上は、代入処理でも特殊な操作をせざるを得ません。

さて、ObjectWrite() が呼び出されるのは、代入文が次の3つの条件を満たしているときです。
※HSP3.3RC1現在の、HSPランタイムの実装を見るかぎりでは。

この条件を満たすときは、通常の代入文の処理ではなく、「左辺を取り出し、その型に対応する ObjectWrite 関数を呼び出す」という処理をします。

ObjectWrite が呼ばれた時点で、次に取り出されるのは右辺の式です。
代入文の右辺には式の列が書かれていますが、これも命令の引数と全く同じ方法で取り出します。

	a(x, y, z) = v1, v2, v3, ...	// 代入文の右辺は式の列

配列の添字は関数の引数と同じ、と先述しましたが、似たような話ですね。代入文は命令文の一種なのです。

いま assoc がすることは、内部変数が参照されていれば右辺の値をそれに代入し、
そうでなければ、右辺の値を通常の代入文のごとく、左辺に Set 関数で代入することです。

//------------------------------------------------
// 連想配列::書き込み
//------------------------------------------------
static void ObjectWrite( PVal* pval, void* data, int vflag )
{
	PVal* pvInner = (PVal*)pval->master;
	
	// assoc への代入
	if ( pvInner == NULL ) {
		if ( vflag != g_vtAssoc ) puterror( HSPERR_INVALID_ARRAYSTORE );	// 右辺の型が不一致
		
		HspVarAssoc_Set( pval, (PDAT*)HspVarAssoc_GetPtr(pval), data );
		code_assign_multi( pval );		// 連続代入の処理
		
	// 内部変数を参照している場合
	} else {
		PVal_assign( pvInner, data, vflag );	// 内部変数への代入処理
		code_assign_multi( pvInner );
	}
	
	return;
}

code_assign_multi() も hpimod の mod_argGetter の関数ですが、かなりややこしいので、詳しくはそちらを参照してください。
連続代入 ( x(0) = 1, 2, 3, ... のように、複数の要素に一度に代入すること ) を行うための関数です。

余談ですが、(ArrayObject + ObjectWrite) と ArrayObjectRead (= ArrayObject + ArrayObjectReadImpl) が対応していますね。
このあたりを上手く整理したいところです。


CAssoc 実装

HspVarProc の定義が完了したので、CAssoc の具体的な実装をします。
すごくシンプルですけどね。

まずはコンストラクタです。

CAssoc::CAssoc()
	: mpMap ( new Map_t )
	, mCnt  ( 0 )
{ }

CAssoc* CAssoc::New()
{
	return new CAssoc();
}

続いて、要素へのアクセス、At() 関数。
キーで検索し、内部変数(PVal*)を取得します。なければ、新たに追加します。

PVal* CAssoc::At( const Key_t& key )
{
	Map_t::iterator iter = mpMap->find( key );
	if ( iter != mpMap->end() ) {
		return iter->second;
		
	// 新しい要素を追加
	} else {
		PVal* pval = NewElem();
		mpMap->insert( MapPair_t( key, pval ) );
	}
	return;
}

内部変数 PVal* を生成・削除するメンバ関数、NewElem(), DeleteElem() 。
PVal* の仕様は後で変えられるようにしたいので、関数にしておきます。

PVal* CAssoc::NewElem()
{
	PVal* pval = (PVal*)hspmalloc( sizeof(PVal) );
	PVal_init( pval );
	return pval;
}

void CAssoc::DeleteElem( PVal* pval )
{
	PVal_free( pval );
	hspfree( pval );
	return;
}

最後に、デストラクタです。

CAssoc::~CAssoc()
{
	if ( mpMap ) {
		// 全要素を解放
		for ( Map_t::iterator iter = mpMap->begin(); iter != mpMap->end(); ++ iter ) {
			DeleteElem( iter->second );
		}
		mpMap->clear();
		
		delete mpMap; mpMap = NULL;
	}
	return;
}

void CAssoc::Delete( CAssoc* self )
{
	delete self;
	return;
}

New() で new を使ったので、当然、Delete() では delete を使用します。


コマンド実装

HSP側から assoc に干渉するための命令コマンドを実装します。
その前に、assoc 型の引数を処理する関数を作っておくと、作業が楽になります。

//------------------------------------------------
// assoc 型の値を受け取る
// 
// @ mpval は assoc 型となる。
//------------------------------------------------
CAssoc* code_get_assoc()
{
	if ( code_getprm() <= PARAM_END ) puterror( HSPERR_NO_DEFAULT );
	if ( mpval->flag != g_vtAssoc ) puterror( HSPERR_TYPE_MISMATCH );
	return *(CAssoc**)g_pHvpAssoc->GetPtr(mpval);
}

//------------------------------------------------
// assoc の内部変数を受け取る
// 
// @ NULL が返る可能性もある
//------------------------------------------------
PVal* code_get_assoc_pval( CAssoc** pAssoc )
{
	PVal* pval = code_get_var();		// hpimod/hsp3plugin_custom.h
	if ( pval->flag != g_vtAssoc ) puterror( HSPERR_TYPE_MISMATCH );
	if ( pAssoc ) { *pAssoc = *(CAssoc**)g_pHvpAssoc->GetPtr(pval); }
	return (PVal*)pval->master;
}

コマンドは実装は、引数を処理し CAssoc のメンバ関数を呼び出すだけの簡単なお仕事なので、ここで掲載するのは以下の2つだけにしておきます。

// cmd_assoc.h (一部)

#include "vt_assoc.h"

// 命令
extern void AssocNew();		// 構築

extern void AssocDim();		// 内部変数を配列にする

// 関数
// (略)

実装は以下のようになります。

// cmd_assoc.cpp

//------------------------------------------------
// 構築 (dim)
//------------------------------------------------
void AssocNew()
{
	PVal* pval = code_getpval();	// assoc 型にする変数
	
	int len[4];		// 各次元の要素数
	for ( int i = 0; i < 4; ++ i ) {
		len[i] = code_getdi(0);
	}
	
	exinfo->HspFunc_dim( pval, g_vtAssoc, 0, len[0], len[1], len[2], len[3] );
	return;
}

//------------------------------------------------
// 内部変数の dim
// 
// @prm: [ assoc("key"), vartype, len1..4 ]
//------------------------------------------------
void AssocDim()
{
	PVal* pvInner = code_get_assoc_pval();
	int   vflag   = code_getdi( pvInner->flag );	// 型タイプ値
	
	int len[4];			// 各次元の要素数
	for ( int i = 0; i < 4; ++ i ) {
		len[i] = code_getdi(0);
	}
	
	// 配列として初期化する
	exinfo->HspFunc_dim( pvInner, vflag, 0, len[0], len[1], len[2], len[3] );
	
	return;
}

こうして並べると、ほぼ同じなのがわかります。一般化した方がいいと思いますね。


ヘッダファイル

それでは、最低限の機能は実装したので、ヘッダファイル(.as)を作って完成とします。

// var_assoc - public header

#ifndef IG_ASSOC_HPI_AS
#define IG_ASSOC_HPI_AS

#regcmd "_hsp3hpi_init@4", "var_assoc.hpi", 1
#cmd assoc            0x000	// assoc型変数を作成

#cmd AssocDim         0x010	// 内部変数を配列にする

#cmd AssocNull        0x200	// null オブジェクト

#endif

おわりに

なんだか、思ったより書きたいことが書けなかった感じがします……。
ので、次は vector 的な何かで、もう一度「連想配列型」をやりたいと思うのですが……どうなるやら。

ではまた次回。

追記(2014.02/15)
プラグインの中の関数が、スクリプトにおけるどの動作に対応しているのかが分かりやすくなるように、スクリプトの例をいくつか挿入。


by 上大

第九章へ