CODESYSで任意の構造体でのMap（HashTable）実装

# 対象読者

- CODESYSでMap（HashTable）の実装方法を知りたい方
- オブジェクト指向プログラミング（OOP）の基礎（インターフェースの実装やポリモーフィズム）を理解している方
- CODESYSまたはTwinCATでST言語を使用した開発経験のある方

# 余談


最近CODESYSにはまっています。
オブジェクト指向を使いながら近代的なプログラミング言語のようにPLCプログラムを書くことができ、TwinCATと比べても価格も安いためです。
TwinCATには以下のようなメリットがあり、価格を取るか、メリットをとるかは、装置規模や導入台数によって変わってくると思います。

- POU毎にテキストファイルで保存されてGit管理がしやすい
- ADSが使えてPC側との通信がしやすい
- IDEがVisual StudioでCODESYSと比べて軽量かつ使いやすい
- ハードウェア（Bekckhoff IPC/EPC）のラインナップが多い
- トライアルのランタイムライセンスが1週間継続（CODESYSは2時間ごとに切れる）し、実機ではない開発端末でのテストがしやすい

# 当記事記載の背景

最近CODESYSで実装したプログラムで、STRING型のIDをKeyに構造体のワーク情報を管理したい場合がありました。
C++やC#といったIT向けのプログラミング言語においては、このようなケースでは一般的にKey-Value形式のCollectionのMap（C#やPythonではDictionaryという名前）を使用します。
三菱電機やキーエンスといったクラシックPLCでこのような機能を実装したい場合には、配列で持たせて、値を取り出したい場合には毎回走査するといった方法になるのでしょうか（クラシックPLCに詳しくないため、もし他に良い方法があれば教えてください）。
ただしその場合には値を取り出すごとにO(N)の処理が走り、処理効率が悪く、ワークの種類数によっては目標とするスキャンタイムに収まらなくなることも出てきます。

CODESYSではC++やC#同様に[CollectionやMap](https://content.helpme-codesys.com/en/libs/Element%20Collections/4.0.0.0/ElementCollections/Interfaces/Collections/fld-Collections.html)がライブラリとして提供されています。
具体的な（インターフェースではない）FBとしては[HashTable](https://content.helpme-codesys.com/en/libs/Element%20Collections/Current/ElementCollections/Function-Blocks/HashTable/fld-HashTable.html)が提供されています。

当記事では INTやSTRINGという基本的な型をKey,Valueとする場合と、独自の構造体（STRUCT）をValueとする場合の実装方法を説明します。

# 基本型をKey,Valueとする場合のHashTableの実装

HashTableの基本的な使い方についてはこちらの[Example: Element Collections](https://content.helpme-codesys.com/en/CODESYS%20Examples/_ex_cds_element_collections.html)を見るのが一番わかりやすいと思います。

基本型をKey,Valueとする場合は[Example: Element Collections](https://content.helpme-codesys.com/en/CODESYS%20Examples/_ex_cds_element_collections.html)のSimpleHashTableExampleにある通りのため、ソースコードを引用し、簡単な解説を行います。

```pascal
// This example shows how to add key value pairs to hash tables and how to get a value by key.
PROGRAM PLC_PRG
VAR
	HashTableFactory : COL.HashTableFactory; // Factory to create a hash table on heap memory.
	itfMap : COL.IMap := HashTableFactory.Create(udiMaxElements := 100); // Creates a hashtable on heap memory. NOTE: Collections that have been created with this Factory are not online change safe.	
	StringElementFactory : COL.StringElementFactory; // Factory to create StringElements on heap memory.
	
	state : ActionState := ActionState.IDLE;
	sKey : STRING := 'Enter a unique key';
	sValue : STRING := 'Enter a value';
	sKeyFind : STRING;
	itfStringKey : COL.IStringElement;
	itfIInstance : FBF.IInstance;
	sValueFound : STRING;
	eError : COL.COLLECTION_ERROR;
	aStringValues : ARRAY[0..99] OF STRING;
	iterator : COL.ListIterator;
	itfElement : COL.IElement;
	itfStringElement : COL.IStringElement;
	i : INT;
	xResult: BOOL;
END_VAR

CASE state OF
	ActionState.IDLE:
		// Idle
		
	ActionState.ADD_ELEMENT:
		// Add key value pair
		eError := itfMap.AddKeyValuePair(itfKey := StringElementFactory.Create(sValue := sKey), itfValue := StringElementFactory.Create(sValue := sValue));
		IF eError = COL.COLLECTION_ERROR.NO_ERROR THEN
			state := ActionState.UPDATE_VISU;
		ELSE
			state := ActionState.IDLE;
		END_IF;
		
	ActionState.FIND_ELEMENT_BY_KEY:
		// Finds a value by key
		itfStringKey := StringElementFactory.Create(sValue := sKeyFind);
		eError := itfMap.GetElementByKey(itfKey := itfStringKey, itfValue => itfElement);
		xResult := __QUERYINTERFACE(itfStringKey, itfIInstance);
		IF xResult THEN
			// Remove the temporary key
			itfIInstance.Dispose();
		END_IF
		
		// Set the found value
		sValueFound := '';
		xResult := __QUERYINTERFACE(itfElement, itfStringElement);
		IF xResult THEN
			sValueFound := itfStringElement.StringValue;
		END_IF
		IF eError = COL.COLLECTION_ERROR.NO_ERROR THEN
			state := ActionState.UPDATE_VISU;
		ELSE
			state := ActionState.IDLE;
		END_IF;
		
	ActionState.UPDATE_VISU:
		// Clear visu array
		FOR i := 0 TO 9 DO
			aStringValues[i] := '';
		END_FOR
		eError := itfMap.Keys(itfIterator := iterator);
		i := 0;
		WHILE iterator.HasNext() DO
			iterator.Next(itfElement => itfElement);
			// Cast IElement to IStringElement
			__QUERYINTERFACE(itfElement, itfStringElement);
			aStringValues[i] := itfStringElement.StringValue;
			i := i + 1;
		END_WHILE
		state := ActionState.IDLE;
END_CASE
```

Exampleから重要な部分を抜粋して説明します。
まず以下で必要なFacotry FBの定義と、HashTableの作成を行っています。

```pascal
HashTableFactory : COL.HashTableFactory; //HashTable自体のFactory FB
itfMap : COL.IMap := HashTableFactory.Create(udiMaxElements := 100); //100個の要素を持つHashTableのインスタンス生成	
StringElementFactory : COL.StringElementFactory; //StringElement型のインスタンスを生成するためのFactory FB
```

ポイントとして、このCollectionライブラリの要素にSTRING型やINT型の値を直接入れることはできず、Key,ValueともIElement型を実装（Implements）したFBとする必要があります。

INT型やSTRINGといった基本型はIElementを実装済みの[IIntElement](https://content.helpme-codesys.com/en/libs/Element%20Collections/Current/ElementCollections/Interfaces/Elements/IIntElement.html#iintelement)や[IStringElement](https://content.helpme-codesys.com/en/libs/Element%20Collections/Current/ElementCollections/Interfaces/Elements/IStringElement.html#istringelement)型のFB（ITF）を生成してくれるFactoryメソッドが用意されているため、これを使用すれば比較的簡単に実装できます。

HashTableへの要素の追加は以下にて実施しています。

```pascal
eError := itfMap.AddKeyValuePair(itfKey := StringElementFactory.Create(sValue := sKey), itfValue := StringElementFactory.Create(sValue := sValue));
```

Key, Valueとも追加時に動的にFBのインスタンスをFactoryで生成して追加してます。

追加済みの要素の取得は以下で実施しています。

```pascal
		itfStringKey := StringElementFactory.Create(sValue := sKeyFind);
		eError := itfMap.GetElementByKey(itfKey := itfStringKey, itfValue => itfElement);
		xResult := __QUERYINTERFACE(itfStringKey, itfIInstance);
		IF xResult THEN
			// Remove the temporary key
			itfIInstance.Dispose();
		END_IF
		
		// Set the found value
		sValueFound := '';
		xResult := __QUERYINTERFACE(itfElement, itfStringElement);
		IF xResult THEN
			sValueFound := itfStringElement.StringValue;
		END_IF
```

追加と比べて取得は少々複雑です。
GetElementByKeyメソッドの返り値はIElement型（コード内ではitfElementという変数）となります。IElement型のままではオブジェクト指向的な意味で抽象度が高すぎて目的の値を取り出せないため、__QUERYINTERFACEでIStringElement型（コード内ではitfStringElementという変数）に変換しています。

こうすることで取得したいSTRING型の値を取り出せます。

# 独自作成の構造体をValueとする場合のHashTableの実装

## サンプル

当記事のサンプルは[こちら](https://github.com/enakata06/CODESYS_HashTable_Struct_Example)のGitHubに置いています。
当記事の内容のみではなく、動作がわかりやすくするためにHashTableを活用した製品登録・編集用のVisualizationも作成してます。

![example-ss.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/eb8caf4d-4be4-3727-a505-4da2e70b1ca3.png)


前章の基本型を使用する場合においてもC#やPythonといったIT向けの言語に比べるとなかなか手間ですが、独自作成の構造体を使用する場合には正直かなりの手間が発生します。

前章までの用意されている基本型の場合と比べ、独自の構造体を使用する場合には以下が必要となります。

- IElementをImplementsした独自Elementの実装
- 独自Element型のFBインスタンスを動的に生成するFactoryの実装

実装のイメージとしては、[Example: Element Collections](https://content.helpme-codesys.com/en/CODESYS%20Examples/_ex_cds_element_collections.html)のSimpleHashTableExampleとElementExampleを組み合わせます。

今回はKeyをSTRING型、Valueを以下の独自の構造体として追加したい場合でご説明します。

```pascal
TYPE ST_Product :
STRUCT
	//製品情報
	id : STRING; 	//製品ID
	name : STRING; 	//製品名
	width : UINT; 	//幅
	length : UINT; 	//長さ
	height : UINT; 	//高さ
END_STRUCT
END_TYPE
```

## IElementをImplementsした独自Elementの実装

### インターフェースの実装

実態がないインターフェースの定義のみのためシンプルに以下の通りです。

```pascal
PRG_ProductSetting.state <>  E_ProductSettingState.Edit
PROPERTY ProductValue : ST_Product
```

### 実態のFBの実装

インターフェースは不要で実態のみの実装にできるとよいですが、FACTORYや値取得時に__QUERYINTERFACEを使用する仕様上、それぞれの実装が必要なようです。

```pascal
// Demo element with two integer values. 
// An element must implement an interface which extends |COL.IElement|.
// This function block extends FBF.InstanceBase to enable dynamic creation via FBFactory.
{attribute 'no_explicit_call' := 'Explicit calls not allowed.'}
FUNCTION_BLOCK FB_ProductElement EXTENDS FBF.InstanceBase IMPLEMENTS IProductElement
VAR_INPUT CONSTANT
	_product : ST_Product;
END_VAR
VAR_OUTPUT
END_VAR
VAR

END_VAR

```

```pascal
// Compares this element with itfElement.
// Returns 0 if the elements are equal, < 0 if the element is less than itfElement, 
// > 0 if the element is greater than itfElement.
// This method will be called from sorted collections (e.g. |COL.SortedList|) to sort the elements.
// IMPORTANT: The underlying value to be compared with MUST NOT be changed during the lifecycle of the object.
METHOD ElementCompareTo : INT
VAR_INPUT
	(* The element to compare*)
	itfElement	: COL.IElement;
END_VAR
VAR
	element : IProductElement;
	xResult : BOOL;
	elementProduct : ST_Product;
END_VAR

// We use integer iInt1 for sorting.
xResult := __QUERYINTERFACE(itfElement, element);
IF xResult THEN
	elementProduct := element.ProductValue;
	IF _product.id < elementProduct.id THEN
		ElementCompareTo := -1;
	ELSIF _product.id > elementProduct.Id THEN
		ElementCompareTo := 1;
	ELSE
		ElementCompareTo := 0;
	END_IF
ELSE
	ElementCompareTo := -1;
END_IF 

```

```pascal
// Returns true of this element and itfElement are equal.
// This method will be called from the function block |COL.HashTable| to find an element by key if the hashcode of an element collides with an other element.
// IMPORTANT: The underlying value to be compared with MUST NOT be changed during the lifecycle of the object.
METHOD ElementEquals : BOOL
VAR_INPUT
	(* The element to compare*)
	itfElement : COL.IElement;
END_VAR
VAR
	element : IProductElement;
	xResult : BOOL;
	elementProduct : ST_Product;
	thisProduct : ST_Product;
END_VAR

xResult := __QUERYINTERFACE(itfElement, element);
elementProduct := element.ProductValue;
thisProduct := ProductValue;
IF xResult THEN
	ElementEquals := thisProduct.id = elementProduct.id;	
ELSE
	ElementEquals := FALSE;
END_IF 
```

```pascal
// Returns the hashcode of this element.
// This method is used by hash based collections (e.g. |Hashtable|) to get the hashcode from a element.
// IMPORTANT: The value of the method MUST NOT be changed during the lifecycle of the object.
METHOD ElementHashCode : LINT
VAR
	p : POINTER TO STRING;
END_VAR

p := ADR(_product.id);
ElementHashCode := COL.HashCodeFromString(p);
```

```pascal
// This methode is called from the factory to give a chance
// to initialize the new fb instance with the content of itfData
METHOD prvInstInit : FBF.ERROR
VAR_INPUT
	itfData : FBF.IData; 
END_VAR
VAR
	pData : POINTER TO FB_Product;
	xOk : BOOL;
	thisProduct : ST_Product;
END_VAR

SUPER^.prvInstInit(itfData);
xOk := __QUERYPOINTER(itfData, pData);
IF xOk THEN
	ProductValue := pData^.info;
//END_IF

```

```pascal
PROPERTY ProductValue : ST_Product
//Get
ProductValue := _product;
//Set
_product := ProductValue;
```

実際に比較など行わない場合には、ElementCompareTo、ElementEquals、ElementHashCodeの実装は適当でも良いと思います。

## Factoryの実装

このソースについて、正直私は全ては理解できていないです。。
必要な部分のみサンプルから修正して実装しています。

```pascal
// IElement factory for INT values
{attribute 'enable_dynamic_creation'}
FUNCTION_BLOCK FB_ProductElementFactory EXTENDS FBF.FactoryBase
VAR
END_VAR

SUPER^();
```

```pascal
(* Creates a IntElement *)
METHOD Create : IProductElement
VAR_INPUT
	inputData : ST_Product;
END_VAR
VAR_OUTPUT
	eError : FBF.ERROR;
END_VAR
VAR
	hInst : CAA.HANDLE;
	itfInst : FBF.IInstance;
	xOk : BOOL;
	itfData : FB_Product;
	pInstance : POINTER TO FB_ProductElement;
END_VAR

(* !!! This code must not be subject to change !!! *)
itfData(info:=inputData);
hInst := SUPER^.prvAllocInstMem(eError => eError);
pInstance := SUPER^.prvGetInstPointer(hInst);
IF pInstance <> 0 AND eError = FBF.ERROR.NO_ERROR THEN

	{implicit on}
	pInstance^.__vfInit();
	pInstance^.FB_Init(TRUE, FALSE);
	{implicit off}

	xOk := __QUERYINTERFACE(pInstance^, itfInst);
	IF xOk THEN
		xOk := SUPER^.prvInstInit(itfInst, hInst, itfData, eError=>eError);
	ELSE
		eError := FBF.ERROR.WRONG_INTERFACE;
	END_IF

	IF NOT xOk THEN
		SUPER^.prvFreeInstMem(hInst);
		Create := CAA.gc_pNULL;
    ELSE
        __QUERYINTERFACE(pInstance^, Create);
	END_IF
END_IF

```

prvInstCount、prvInstPoolExtendsFactor、prvInstSizeはサンプルのままのため省略します。

Factoryで動的にCreateする実態のために以下のFB実装が必要です。構造体を直接生成できたらよかったですが、FBF.InstanceDataを継承する必要があるようで（※2）、別途作成が必要です。

※2　CODESYSは構造体はPOUではなくDUTというデータを扱うものであり、C++のようにclass（FB）のような役割を持たせられません。

```pascal
{attribute 'enable_dynamic_creation'}
{attribute 'hide'}
FUNCTION_BLOCK FB_Product EXTENDS FBF.InstanceData
VAR_INPUT
	info : ST_PRODUCT;
END_VAR

(* !! Do not add any executable code here !! *)
SUPER^();
```

## Applicationの設定（2025/2/23追記）

動的なメモリ使用のためにApplicatonを右クリック > プロパティ > Application Build Optionsの"Use dynamic memory allocation"にチェックを入れる必要があるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/31055331-be57-4b12-9bd9-77562066cdeb.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1067155/42215fd8-c4fc-4537-b8cb-1b9eb192cadd.png)

同様のソースコードを仕事で使用している際にHashTableの値の更新時などに画面がフリーズし、IPC自体の処理もおかしくなってしまい、メモリ破壊が疑われてしまうような事象がありました。

CODESYSの設定を色々見ている中で、このような設定があることに気づきました。
https://content.helpme-codesys.com/en/CODESYS%20Development%20System/_cds_operator_new.html


## 使用方法例

### 要素の追加

```pascal
PROGRAM PRG_ProductSetting
VAR
	HashTableFactory : COL.HashTableFactory; // Factory to create a hash table on heap memory.
	productMap : COL.IMap3 := HashTableFactory.Create(udiMaxElements := 100); // Creates a hashtable on heap memory. NOTE: Collections that have been created with this Factory are not online change safe.	
	productElementFactory : FB_ProductElementFactory;
	
	eError : COL.COLLECTION_ERROR;
	StringElementFactory : COL.StringElementFactory; // Factory to create StringElements on heap memory.
	keyElement : COL. IStringElement;
END_VAR

keyElement := StringElementFactory.Create(sValue := visuProduct.id);
eError := productMap.AddKeyValuePair(itfKey := keyElement, itfValue := productElementFactory.Create(visuProduct));
```

作成したFB_ProductElementFactoryを使用してIProductElement型の要素としてproductMapに追加しています。

### 値の取得

```pascal
METHOD GetProductInfo : ST_Product
VAR_INPUT
	id : STRING;
END_VAR

VAR
	itfStringKey : COL.IStringElement;
	itfIInstance : FBF.IInstance;
	eError : COL.COLLECTION_ERROR;
	itfElement : COL.IElement;
	itfProductElement : IProductElement;
	StringElementFactory : COL.StringElementFactory; // Factory to create StringElements on heap memory.
	xResult: BOOL;
END_VAR

//MapからProductインスタンスを取得
itfStringKey := StringElementFactory.Create(sValue := id);
eError := productMap.GetElementByKey(itfKey := itfStringKey, itfValue => itfElement);
IF eError <> COL.COLLECTION_ERROR.NO_ERROR THEN
	RETURN;
END_IF

//キーの確認
xResult := __QUERYINTERFACE(itfStringKey, itfIInstance);
IF xResult THEN
	// Remove the temporary key
	itfIInstance.Dispose();
END_IF

//IElement形式からIProductElementに変換
xResult := __QUERYINTERFACE(itfElement, itfProductElement);
IF xResult THEN
	GetProductInfo := itfProductElement.ProductValue;
END_IF
```

Mapから直接取得できる型はIElement型のため、__QUERYINTERFACEでIProductElement型に変換することで値を取り出せます。

# 最後に

独自構造体をValueとするHashMapの実装は[Example: Element Collections](https://content.helpme-codesys.com/en/CODESYS%20Examples/_ex_cds_element_collections.html)の組み合わせではありますが、独自のElement実装方法など、きちんと理解して動作させるまでに結構時間がかかりました。そのため、同様のことをしたい人の理解を促進できるように、今回記事にして皆さんに共有した次第です。

Pythonはほとんど型を意識せずに使用できますし、C#ではジェネリクスを使用して簡単に独自classのDictionaryを実装できます。そのため、IT向け言語に比べるとCODESYSでのMapの実装はかなり面倒に感じます。

しかし、Mapの実装は従来のクラシックPLCではできない実装であり、Mapを使いこなせればソフトウェアPLCであるCODESYSを使用するメリットを感じられるのではないでしょうか！