➊ はじめに
本記事は、ジャンプテーブルを用いた実践テクニックに焦点を当てています⛄
イベントドリブン型のシステム開発においては、特に状態遷移
とイベント処理
が重要となります。この状態遷移とイベント処理を管理する方法の一つとして、非常に有用なテクニックとしてジャンプテーブルがあります⛷️💨
C言語初学者は、基本要素(if文、for文、switch文など)やライブラリ(printf、mq_send、mq_receiveなど)を学ぶことで様々なプログラムを作成できるようになりますが、ジャンプテーブルは、C言語の教科書にあまり掲載されていないテクニックのため、ここで詳しく解説します🍢
ジャンプテーブルとは、机の上でジャンプすることではありません。上の画像は、AIが考えるジャンプテーブルであり、本記事とはなんら関係がございませんので、あしからず😂
➋ 具体的に何をするか
バンバーガーの自動販売機を制御するシステムを開発するつもりで、設計からプログラミングまで、ジャンプテーブルを使った開発方法を説明していきます。
⬢ ジャンプテーブルとは
まず、ジャンプテーブルとは何かについてご説明します。
ジャンプテーブルとは、関数や処理を実行するためのプログラミングテクニックです
。通常、プログラム内の条件分岐やswitch文を使用して処理の制御を行いますが、ジャンプテーブルはこれらの代替手法として使われます。
主にイベントドリブン型のシステム開発などに使用され、とある状態のときに、発生したイベントに応じて処理を行いたいときに有効な手段となります。ジャンプテーブルは、状態やイベントを識別し、それに対応する関数や処理に素早くジャンプすることができるため、処理の効率性と保守性を向上させます。
➌ 自動販売機の仕様
まずは、ハンバーガーの自動販売機の仕様を簡単に作ります。今回は、簡単にするために、以下の仕様パターン1
と仕様パターン2
の2つのユースケースがあり、そこから詳細の仕様を作りました。
実際のお金投入システムでは、合計金額やお釣りと言った処理も必要となりますが、複雑となりますので、本記事では簡易化した特別仕様とします。
⬢ 仕様パターン1
- 自販機のシステム起動時は、お金が0円。
- お金は100円のみを投入可能。(100円以外は投入不可。一度お金を投入すると、投入口が機械的にロックされ複数枚の投入は不可。)
- 100円を入れた状態で購入ボタンを押下するとハンバーガーが買える。
- ユーザーがハンバーガーを取り出すと、「ありがとうございました。」と喋る。
⬢ 仕様パターン2
- お金を入れた状態で、返却レバーを押下すると、お金が返却される。
➍ 状態遷移図
この自動販売機の仕様(パターン1、パターン2)をまとめたものが、以下の「状態遷移図」になります。青の四角で囲まれたところが状態、緑の文字がイベントになります。
ここから、以下の「3つの状態」と「4つのイベント」があることが整理できました。
-
状態
- ノーマル状態
- 商品購入待ち状態
- 商品購入済み状態
-
イベント
- お金投入
- 商品購入ボタン押下
- 返却レバー押下
- 商品取り出し完了
➎ 状態遷移表
「状態」と「イベント」が整理できたので、ここからよりプログラムっぽい「状態遷移表」を作成してみましょう。ここでは列に状態、行にイベントとしました(列と行は逆でもOK)。状態遷移表のことをマトリクスとも呼びます。状態とイベントの交点の上部に処理内容を、下部に遷移先状態を書いています。
この表の見方ですが、一例をあげて説明します。
例えば、「ノーマル状態」のとき、「お金投入イベント」が発生したら、金額を+100円とし、状態を「商品購入待ち状態」へ遷移させます。「ノーマル状態」のとき、それ以外のイベントが発生しても、何もしない(受け付けない)という感じです。
この状態遷移表(マトリクス)を作ることで、すべての状態において、どのイベントが発生しても、無視を含めなんの処理を行うのかを明確にすることができます。
➏ ジャンプテーブルの書き方
設計はできたので、実際のジャンプテーブルの書き方についてです。
本章では、書き方の説明を目的としますので、Snippet(ソースコードの断片)のみ記載します。ソースコード全文(ヘッダ含む)は、➐に記載します。
⬢ 状態やイベントの列挙型定義
まずは、状態やイベントを列挙型で定義します。状態遷移表を見ながらデータを設定していきます。
// 列挙型で状態を定義
enum VendingState
{
STATE_NORMAL, // ノーマル状態
STATE_PURCHASE_WAITING, // 商品購入待ち状態
STATE_PURCHASED, // 商品購入済み状態
STATE_MAX // 状態MAX値
};
// 列挙型でイベントを定義
enum VendingEvent
{
EVENT_INSERT_MONEY, // お金投入イベント
EVENT_PURCHASE_BUTTON_PRESSED, // 商品ボタン押下イベント
EVENT_RETURN_LEVER, // 返却レバー押下イベント
EVENT_COMPLETE_PURCHASE, // 商品購入完了イベント
EVENT_MAX // イベントMAX値
};
⬢ ジャンプテーブルの型定義
ジャンプテーブルの関数ポインタの型を定義します。ここでは「void (*VendingFunction)」として定義しました。続く「(unsigned int *)」は引数用です。
// ジャンプテーブル型定義
typedef void (*VendingFunction)(unsigned int *);
// プロトタイプ宣言
void insertMoney(unsigned int *money);
void purchaseButtonPressed(unsigned int *money);
void returnLever(unsigned int *money);
void completePurchase(unsigned int *money);
void printState();
void printMenu();
⬢ ジャンプテーブルの実態
先ほど定義したジャンプテーブルの型から、実メモリ上に格納されるジャンプテーブルを作成します。このテーブルには、呼び出し関数のアドレスが登録されます。
➎で作成した状態遷移表を見ながらデータを設定していきます。
// ジャンプテーブルを作成し、関数ポインタを格納
const VendingFunction jumpTable[EVENT_MAX][STATE_MAX] = {
//ノーマル状態 商品購入待ち状態 商品購入済み状態
{insertMoney, NULL, NULL}, // お金投入イベント
{NULL, purchaseButtonPressed, NULL}, // 商品購入ボタン押下イベント
{NULL, returnLever, NULL}, // 返却レバー押下イベント
{NULL, NULL, completePurchase} // 商品購入完了イベント
};
先ほど➎で作った設計書(状態遷移表)も、関数名を入れたものにUpdateしておきましょう。
⬢ ジャンプテーブルの呼び出し方
ジャンプテーブルの呼び出し方は、以下の通りです。それぞれ「イベント」、「状態」、「引数」を設定してコールします。ここでevent
、state
などは、テーブルのMAX値を超えないように注意してください。また、ジャンプテーブルが指し示した場所が、「NULL」である場合は、NOP(No Operation=何もしない)、エラー処理を行う等の処理をさせます。
jumpTable[event][state](&money); // ジャンプテーブルCall
➐ ソースコード全文
ソースコード全文です。ヘッダ(vendingMachine.h
)とソースコード(vendingMachine.c
)に分けています。
(1) ヘッダ全文
ヘッダ全文を記載していきます。
#ifndef VENDINGMACHINE_H
#define VENDINGMACHINE_H
// 100円をマクロ定義
#define MONEY100YEN 100
// 列挙型で状態を定義
enum VendingState
{
STATE_NORMAL, // ノーマル状態
STATE_PURCHASE_WAITING, // 商品購入待ち状態
STATE_PURCHASED, // 商品購入済み状態
STATE_MAX // 状態MAX値
};
// 列挙型でイベントを定義
enum VendingEvent
{
EVENT_INSERT_MONEY, // お金投入イベント
EVENT_PURCHASE_BUTTON_PRESSED, // 商品ボタン押下イベント
EVENT_RETURN_LEVER, // 返却レバー押下イベント
EVENT_COMPLETE_PURCHASE, // 商品購入完了イベント
EVENT_MAX // イベントMAX値
};
// ジャンプテーブル型定義
typedef void (*VendingFunction)(unsigned int *);
// プロトタイプ宣言
void insertMoney(unsigned int *money);
void purchaseButtonPressed(unsigned int *money);
void returnLever(unsigned int *money);
void completePurchase(unsigned int *money);
void printState();
void printMenu();
#endif
(2) ソースコード全文
ソースコード全文を記載していきます。
#include <stdio.h>
#include "vendingMachine.h"
// 初期状態を設定
enum VendingState state = STATE_NORMAL;
int main()
{
// ジャンプテーブルを作成し、関数ポインタを格納
const VendingFunction jumpTable[EVENT_MAX][STATE_MAX] = {
//ノーマル状態 商品購入待ち状態 商品購入済み状態
{insertMoney, NULL, NULL}, // お金投入イベント
{NULL, purchaseButtonPressed, NULL}, // 商品購入ボタン押下イベント
{NULL, returnLever, NULL}, // 返却レバー押下イベント
{NULL, NULL, completePurchase} // 商品購入完了イベント
};
unsigned int money = 0; // お金の初期値を0円と設定
unsigned int choice; // UI番号
unsigned int event; // イベント番号
printf("自動販売機\n");
while (1)
{
printf("---------------------------------------------------- \n");
printf("現在の状態: ");
printState();
printf("現在の金額: %d円\n", money);
printf("選択してください (1-4):\n");
printMenu();
printf("▶ ");
if (scanf("%u", &choice) == 1)
{
event = choice - 1; // UIから内部イベント番号へ変換
if (event < EVENT_MAX && state < STATE_MAX)
{
if (jumpTable[event][state] != NULL)
{
jumpTable[event][state](&money); // ジャンプテーブルCall
}
else
{
printf("無効な選択です。\n");
}
}
else
{
printf("無効な選択です。\n");
}
}
else
{
printf("入力エラー。\n");
// 入力バッファをクリアして無効な入力を捨てます
while (getchar() != '\n')
;
}
}
return 0;
}
// お金投入
void insertMoney(unsigned int *money)
{
printf("お金を投入しました: %d円\n", MONEY100YEN);
*money += MONEY100YEN; // お金を100円増やす
// 商品購入待ち状態へ遷移
state = STATE_PURCHASE_WAITING;
}
// 商品購入
void purchaseButtonPressed(unsigned int *money)
{
printf("%d円の商品を購入しました\n", MONEY100YEN);
if (*money >= MONEY100YEN)
{
*money -= MONEY100YEN; // お金から100円引く
// 商品購入済み状態へ遷移
state = STATE_PURCHASED;
}
else
{
printf("お金が足りません。\n");
}
}
// 返却レバー押下
void returnLever(unsigned int *money)
{
printf("返却レバーを引きました: %d円返却\n", *money);
*money = 0; // お金を0円にリセット
// ノーマル状態へ遷移
state = STATE_NORMAL;
}
// 商品取り出し完了
void completePurchase(unsigned int *money)
{
printf("ありがとうございました.\n");
// ノーマル状態へ遷移
state = STATE_NORMAL;
}
// 現在の状態を表示する関数
void printState()
{
switch (state)
{
case STATE_NORMAL:
printf("ノーマル状態\n");
break;
case STATE_PURCHASE_WAITING:
printf("商品購入待ち\n");
break;
case STATE_PURCHASED:
printf("商品購入済み状態\n");
break;
default:
printf("無効な状態です\n");
break;
}
}
// メニューを表示する関数
void printMenu()
{
switch (state)
{
case STATE_NORMAL:
printf("1. お金投入\n");
break;
case STATE_PURCHASE_WAITING:
printf("2. 商品押下\n");
printf("3. 返却レバー押下\n");
break;
case STATE_PURCHASED:
printf("4. 商品を取得しました\n");
break;
default:
printf("無効な状態です\n");
break;
}
}
➑ 実行結果
それでは、正常系として、パターン1、パターン2それぞれ実行してみたいと思います。
■ 実行環境
- Ubuntu 22.04.3 LTS (GNU/Linux 5.10.16.3-microsoft-standard-WSL2 x86_64)
- gcc version 11.4.0
(1) コンパイル & 実行
vendingMachine.h
とvendingMachine.c
があることを確認し、以下のコマンドを打って、コンパイルを実施してください。ちなみに「vending machine
」とは、自動販売機という意味です。
-
コンパイルオプション補足
- gccのオプション「
-g
」を使用して、gdbでデバッグできるように、実行可能ファイルにデバッグ情報を含めます。 - gccのオプション「
-o
」を使用して、コンパイラが生成する実行可能ファイルのファイル名を指定します。
- gccのオプション「
gcc -g vendingMachine.c -o vendingMachine
実行は、以下コマンドでできます。
./vendingMachine
デバッグしながら実行したい方は、こちらでできます。
gdb ./vendingMachine
gdbのコマンド「l(list)、b(break)、r(run)、c(continue)、s(step)、n(next)、p(print)、q(quit)」などを駆使して、デバッグしてください。
(2) パターン1 実行結果
パターン1の正常系を確かめましょう。中断は「Ctrl+C」です。
自動販売機
----------------------------------------------------
現在の状態: ノーマル状態
現在の金額: 0円
選択してください (1-4):
1. お金投入
▶ 1
お金を投入しました: 100円
----------------------------------------------------
現在の状態: 商品購入待ち
現在の金額: 100円
選択してください (1-4):
2. 商品押下
3. 返却レバー押下
▶ 2
100円の商品を購入しました
----------------------------------------------------
現在の状態: 商品購入済み状態
現在の金額: 0円
選択してください (1-4):
4. 商品を取得しました
▶ 4
ありがとうございました.
----------------------------------------------------
現在の状態: ノーマル状態
現在の金額: 0円
選択してください (1-4):
1. お金投入
▶
(3) パターン2 実行結果
パターン2の正常系を確かめましょう。中断は「Ctrl+C」です。
自動販売機
----------------------------------------------------
現在の状態: ノーマル状態
現在の金額: 0円
選択してください (1-4):
1. お金投入
▶ 1
お金を投入しました: 100円
----------------------------------------------------
現在の状態: 商品購入待ち
現在の金額: 100円
選択してください (1-4):
2. 商品押下
3. 返却レバー押下
▶ 3
返却レバーを引きました: 100円返却
----------------------------------------------------
現在の状態: ノーマル状態
現在の金額: 0円
選択してください (1-4):
1. お金投入
▶
➒ 補足
本記事についての「補足」や「ジャンプテーブルの要点」について記載します。
(1) 補足
今回は説明のため簡易な実装としたため、イベントはUIで発生させていますが、実際、大規模なシステム開発となる場合は、メッセージキュー待ちのループが基本になると思いますので、ジャンプテーブルの引数はメッセージ型、イベント自体はメッセージ番号になることが多いと思います。
(2) ジャンプテーブルの要点
ジャンプテーブルの要点を以下に記載します。
用途 | ジャンプテーブルは、複数の選択肢から処理を選択するためのデータ構造です。通常、条件分岐や多数のケース文を置き換えることができます。 |
構造 | ジャンプテーブルは通常、配列や連想配列(マップ)の形式を取ります。各要素は処理への参照(関数へのポインタ)を保持します。 |
実行 | プログラムが特定の条件やイベントに対して適切な処理を選択するために、ジャンプテーブル内の適切な要素を参照します。これにより、プログラムの制御フローがスマートになり、コードの簡略化が可能となります。 |
効果 | ジャンプテーブルの使用は、可読性を向上させ、保守性を高めることができます。特に多くの条件分岐やケース文がある場合、ジャンプテーブルはコードの管理を容易にします。 |
注意事項 | ジャンプテーブルは適切に設計され、エラーハンドリングが行われない場合にセキュリティの問題を引き起こす可能性があるため、慎重に使用する必要があります。また、ジャンプテーブルの選択インデックスが不正な場合に備えて、適切なエラーチェックが必要です。 |
➓ さいごに
若手のときに、大規模な組み込み系のシステム開発(イベントドリブン型)をメインで担当したことがあるのですが、ジャンプテーブルの勉強をしていたおかげて、設計&製造がスムーズにでき、なんとか最後までやり遂げることができました。ジャンプテーブルが万能という訳ではないですが、C言語でシステム開発を行う際は、こんなやり方もあるよというのを知っておいて、適した開発方法をチョイスできると良いかと思います。
⬢ イイ思い出😊💭
若い頃は、システム開発のため休日出勤や大残業などをして一生懸命頑張っておりました。あんなに苦労してやっていたのに、長いことC言語を使用していなかったので、だいぶ忘れてきていますね(忘れる前に共有しないと😅)。人生でぶっ倒れるほど頑張ったってこと、あんまりないと思います。ただ、この頑張りがあったからこそ、ちょっとやそっとのことでは、くじけず頑張れているというのも本当のところです。いまではイイ思い出です…最後に思い出にふけってしまいましたが、自分が作ったシステムが世の中で動いているって、ちょっと感動的ですよね。
⬢ Advent Calendar アーカイブ👣
・2022 【おとなの社会学】ポイント経済の仕組み
・2021 【webアプリ】画像の背景透過処理を行うwebサービスを作ってみた
・2020 【簡単】TeachableMachineを利用して「ゴクリAI認識」webアプリを作ろう