はじめに
固定長バッファを管理領域として、リングバッファによりキューとスタックを実現する実装の解説です。何度目の実装だ。
C89準拠のつもりで書いてます。でもstdint.h
は使っています。これだけは使わせて欲しい。
キューは主にイベントメッセージや受信パケットのような順序性のあるデータの管理に用います。スタックは順序性関係なく、複数のシーケンスで一つのリソースプールを共有する時などに使います。あらかじめ決まったメモリサイズでメッセージを扱う場合、各シーケンスではメッセージをキューイングするためにキューが必要になりますし、そのメッセージを覚えておく領域はメッセージプールとしてスタックで管理しておきたくなります。(リソースプールをキューで管理しても問題は無いですが……。)
とりあえず実用上、一つの実装でキューとスタックを実現できるので便利です。
コードと解説
API(ヘッダファイル)
/* Include guard */
#ifndef FIXED_SIZE_RING_BUFFER_H_INCLUDED
#define FIXED_SIZE_RING_BUFFER_H_INCLUDED
/*****************************************************************************
* Header Include
*****************************************************************************/
#include <stddef.h>
#include <stdint.h>
/*****************************************************************************
* Type Definition
*****************************************************************************/
typedef void* FixedSizeRingBufferElement;
typedef struct FixedSizeRingBuffer_ {
FixedSizeRingBufferElement* buffer_for_elements_;
uint32_t max_elements_count_;
uint32_t wp_;
uint32_t rp_;
} FixedSizeRingBuffer;
/*****************************************************************************
* Function Declaration
*****************************************************************************/
/* インスタンスを構築する */
void FixedSizeRingBuffer_Construct(
FixedSizeRingBuffer* frb_object_memory,
FixedSizeRingBufferElement buffer_for_elements[],
uint32_t max_elements_count);
/* インスタンスを破棄する */
void FixedSizeRingBuffer_Destruct(FixedSizeRingBuffer* frb);
/* 積荷の数を取得する */
uint32_t FixedSizeRingBuffer_GetNumElements(const FixedSizeRingBuffer* frb);
/* 空き領域の数を取得する */
uint32_t FixedSizeRingBuffer_GetNumBlanks(const FixedSizeRingBuffer* frb);
/* Enqueue可能か */
int FixedSizeRingBuffer_IsEnqueueable(const FixedSizeRingBuffer* frb);
/* Dequeue可能か */
int FixedSizeRingBuffer_IsDequeueable(const FixedSizeRingBuffer* frb);
/* Push可能か */
int FixedSizeRingBuffer_IsPushable(const FixedSizeRingBuffer* frb);
/* Pop可能か */
int FixedSizeRingBuffer_IsPoppable(const FixedSizeRingBuffer* frb);
/* Enqueue
* 容量オーバ時の結果は未定義
*/
void FixedSizeRingBuffer_Enqueue(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element);
/* Push
* 容量オーバ時の結果は未定義
*/
void FixedSizeRingBuffer_Push(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element);
/* Dequeue
* 積荷が無い場合の結果は未定義
*/
FixedSizeRingBufferElement FixedSizeRingBuffer_Dequeue(
FixedSizeRingBuffer* frb);
/* Pop
* 積荷が無い場合の結果は未定義
*/
FixedSizeRingBufferElement FixedSizeRingBuffer_Pop(FixedSizeRingBuffer* frb);
/* チェックありEnqueue
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_EnqueueWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element);
/* チェックありPush
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_PushWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element);
/* チェックありDequeue
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_DequeueWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement* dequeued_element);
/* チェックありPop
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_PopWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement* popped_element);
#endif /* FIXED_SIZE_RING_BUFFER_H_INCLUDED */
インスタンスの型定義
FixedRingBuffer
構造体がインスタンスの型です。定義を公開しているのは、ユーザーコード側でメモリ上に配置しやすくするためです。
定義を隠蔽してしまうと、インスタンス生成時にインスタンス構築用のメモリを指定する必要があり、その場合はメモリアラインメントとインスタンスサイズをユーザーコード側で把握できる仕組みを用意する必要があります。
FixedRingBufferPayload
をvoid*
で定義していますが、これは単に一つの定義でなるべく多くの型の値に対応できるようにvoid*
を選んだだけです。void*
の表現範囲がuint32_t
より狭い可能性もあるので、用途にあわせてvoid*
の代わりにuint8_t
やuint32_t
で定義する事をお勧めします。C言語では異なる型に対して関数を抽象化して定義する事ができない点が非常に不便です。
APIの種類
- インスタンスの構築/破棄
- 要素数取得
- 積荷操作系の実行可否チェック
- 積荷操作系(キュー、スタック)
- チェックを伴う積荷操作系
実装
/*****************************************************************************
* Header Include
*****************************************************************************/
#include <fixed_size_ring_buffer.h>
#include <stddef.h>
#include <stdint.h>
/*****************************************************************************
* Macro
*****************************************************************************/
#define FIXEDSIZERINGBUFFER_LOCAL_FALSE (1 == 0)
#define FIXEDSIZERINGBUFFER_LOCAL_TRUE (!FIXEDSIZERINGBUFFER_LOCAL_FALSE)
/*****************************************************************************
* Function Definition
*****************************************************************************/
/* インスタンスを構築する */
void FixedSizeRingBuffer_Construct(
FixedSizeRingBuffer* frb_object_memory,
FixedSizeRingBufferElement buffer_for_elements[],
uint32_t max_elements_count)
{
FixedSizeRingBuffer* frb = frb_object_memory;
frb->buffer_for_elements_ = buffer_for_elements;
frb->max_elements_count_ = max_elements_count;
frb->wp_ = 0U;
frb->rp_ = 0U;
return;
}
/* インスタンスを破棄する */
void FixedSizeRingBuffer_Destruct(FixedSizeRingBuffer* frb)
{
return;
}
/* 積荷の数を取得する */
uint32_t FixedSizeRingBuffer_GetNumElements(const FixedSizeRingBuffer* frb)
{
return (frb->wp_ - frb->rp_);
}
/* 空き領域の数を取得する */
uint32_t FixedSizeRingBuffer_GetNumBlanks(const FixedSizeRingBuffer* frb)
{
return (frb->max_elements_count_ - FixedSizeRingBuffer_GetNumElements(frb));
}
/* Enqueue可能か */
int FixedSizeRingBuffer_IsEnqueueable(const FixedSizeRingBuffer* frb)
{
return (0UL < FixedSizeRingBuffer_GetNumBlanks(frb));
}
/* Dequeue可能か */
int FixedSizeRingBuffer_IsDequeueable(const FixedSizeRingBuffer* frb)
{
return (0UL < FixedSizeRingBuffer_GetNumElements(frb));
}
/* Push可能か */
int FixedSizeRingBuffer_IsPushable(const FixedSizeRingBuffer* frb)
{
return FixedSizeRingBuffer_IsEnqueueable(frb);
}
/* Pop可能か */
int FixedSizeRingBuffer_IsPoppable(const FixedSizeRingBuffer* frb)
{
return FixedSizeRingBuffer_IsDequeueable(frb);
}
/* Enqueue
* 容量オーバ時の結果は未定義
*/
void FixedSizeRingBuffer_Enqueue(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
uint32_t rounded_wp = frb->wp_ % frb->max_elements_count_;
frb->buffer_for_elements_[rounded_wp] = element;
frb->wp_++;
return;
}
/* Push
* 容量オーバ時の結果は未定義
*/
void FixedSizeRingBuffer_Push(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
FixedSizeRingBuffer_Enqueue(frb, element);
return;
}
/* Dequeue
* 積荷が無い場合の結果は未定義
*/
FixedSizeRingBufferElement FixedSizeRingBuffer_Dequeue(FixedSizeRingBuffer* frb)
{
FixedSizeRingBufferElement element;
element = frb->buffer_for_elements_[frb->rp_];
frb->rp_++;
if (frb->max_elements_count_ <= frb->rp_) {
frb->rp_ -= frb->max_elements_count_;
frb->wp_ -= frb->max_elements_count_;
}
return element;
}
/* Pop
* 積荷が無い場合の結果は未定義
*/
FixedSizeRingBufferElement FixedSizeRingBuffer_Pop(FixedSizeRingBuffer* frb)
{
uint32_t rounded_wp;
frb->wp_--;
rounded_wp = frb->wp_ % frb->max_elements_count_;
return frb->buffer_for_elements_[rounded_wp];
}
/* チェックありEnqueue
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_EnqueueWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
if (!FixedSizeRingBuffer_IsEnqueueable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
FixedSizeRingBuffer_Enqueue(frb, element);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
/* チェックありPush
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_PushWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
if (!FixedSizeRingBuffer_IsPushable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
FixedSizeRingBuffer_Push(frb, element);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
/* チェックありDequeue
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_DequeueWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement* dequeued_element)
{
if (!FixedSizeRingBuffer_IsDequeueable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
*dequeued_element = FixedSizeRingBuffer_Dequeue(frb);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
/* チェックありPop
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_PopWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement* popped_element)
{
if (!FixedSizeRingBuffer_IsPoppable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
*popped_element = FixedSizeRingBuffer_Pop(frb);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
マクロ
#define FIXEDSIZERINGBUFFER_LOCAL_FALSE (1 == 0)
#define FIXEDSIZERINGBUFFER_LOCAL_TRUE (!FIXEDSIZERINGBUFFER_LOCAL_FALSE)
FIXEDSIZERINGBUFFER_LOCAL_FALSE
とFIXEDSIZERINGBUFFER_LOCAL_TRUE
は、他の定義名と競合しないようにモジュール名を接頭辞としたfalse値とtrue値の定義です。C言語にはbool型とbool型定数が無いので、それぞれ偽になる条件式と真になる条件式で定義しています。
C言語は条件式評価の際に整数型0を偽、0以外を真と評価するように規格で定められているので、偽を(0)
で定義する流儀は多いです。ですが、(0)
はあくまで整数値なので個人的には気持ち悪いです。偽を条件式で定義しておくと、整数型へ暗黙的に変換はできてしまいますが、条件式と整数の比較のような型の異なる式を静的解析ツールで解析すると警告してくれたりする(MISRA-Cチェッカー?)ので、ここでは偽が自明である条件式で偽値を定義しています。要は気分の問題です。0が偽なのは自明っぽく無い(規格を知らないとわからない)ですが、(1==0)
が偽なのは直感的にわかるでしょう。
ちなみに整数値を条件式(真偽値)にキャストするために!!
をつけるというテクニックも存在します。bool型が存在しないためだけにこんな不毛なことを……。
構築/破棄
/* インスタンスを構築する */
void FixedSizeRingBuffer_Construct(
FixedSizeRingBuffer* frb_object_memory,
FixedSizeRingBufferElement buffer_for_elements[],
uint32_t max_elements_count)
{
FixedSizeRingBuffer* frb = frb_object_memory;
frb->buffer_for_elements_ = buffer_for_elements;
frb->max_elements_count_ = max_elements_count;
frb->wp_ = 0U;
frb->rp_ = 0U;
return;
}
第一引数frb_object_memory
は、スタック上などのコンパイラが自動配置したオブジェクトのメモリを指定してもらう事を想定しています。コンパイラが自動配置したオブジェクトのメモリであればサイズもアラインメントも正しい事が保証されているので、このオブジェクトメモリに対してインスタンス構築関数内でそのまま読み書きできます。
ここでvoid*
型でインスタンス構築用のメモリを渡すような設計にすると、それだけではサイズもアラインメントも不明なので、メモリサイズを渡してもらうための引数の追加と、さらにアラインメントを調整したメモリアドレスを切り出してインスタンスに割り当てる必要が出てきます。アラインメント調整のためにメモリアドレスを切り出すと、メモリサイズが足りなくなる可能性もあります。そこまで考慮するとユーザーコードには、正味のインスタンスサイズより大きい、最大限アラインメント調整を行っても問題無いサイズを伝える必要があります。void*
でインスタンス用のメモリを渡す方法は、場合によってはメモリも無駄にしますし、モジュール実装の手間やコードサイズも増えてしまうのでデメリットが多いです。
これらの問題を一挙に解決するために、FixedSizeRingBuffer
構造体の定義を公開して、コンパイラで自動配置可能にしています。構造体の定義を公開するとユーザーコードで構造体メンバを直接操作できてしまうというデメリットがありますが、これはC言語なので仕方ないところでもあり、メンバを直接操作しないという紳士協定をユーザーコードを書くプログラマが守ってくれる事を期待しています。
第二引数と第三引数はそれぞれこの固定長リングキューの管理領域の指定です。max_elements_count
はbyteサイズではなく要素数です。使い方は以下を想定しています。
FixedRingBuffer frb;
FixedRingBufferPayload buffer_for_elements[4];
FixedRingBuffer_Construct(&frb, buffer_for_elements, 4);
構築直後の要素数はゼロです。
/* インスタンスを破棄する */
void FixedSizeRingBuffer_Destruct(FixedSizeRingBuffer* frb)
{
return;
}
何もすることがないので単にreturn
しているだけの実装です。
要素数の取得API
/* 積荷の数を取得する */
uint32_t FixedSizeRingBuffer_GetNumElements(const FixedSizeRingBuffer* frb)
{
return (frb->wp_ - frb->rp_);
}
/* 空き領域の数を取得する */
uint32_t FixedSizeRingBuffer_GetNumBlanks(const FixedSizeRingBuffer* frb)
{
return (frb->max_elements_count_ - FixedSizeRingBuffer_GetNumElements(frb));
}
固定長バッファに対する書き込み位置wp_
とrp_
の差が積荷の数です。全容量から積荷の数を引けば空き容量です。インスタンスの内容を操作しないのでconst*
で引数を定義します。
チェック系API
容量上限だった場合にEnqueueやPushが可能かどうか、また、積荷ゼロだった場合にDequeueやPopが可能かどうかを事前にチェックするためのAPIです。
単に積荷の数によって操作可能かどうかが決まるので、積荷数を取得するAPIのラッパー実装になっています。
/* Enqueue可能か */
int FixedSizeRingBuffer_IsEnqueueable(const FixedSizeRingBuffer* frb)
{
return (0UL < FixedSizeRingBuffer_GetNumBlanks(frb));
}
/* Dequeue可能か */
int FixedSizeRingBuffer_IsDequeueable(const FixedSizeRingBuffer* frb)
{
return (0UL < FixedSizeRingBuffer_GetNumElements(frb));
}
/* Push可能か */
int FixedSizeRingBuffer_IsPushable(const FixedSizeRingBuffer* frb)
{
return FixedSizeRingBuffer_IsEnqueueable(frb);
}
/* Pop可能か */
int FixedSizeRingBuffer_IsPoppable(const FixedSizeRingBuffer* frb)
{
return FixedSizeRingBuffer_IsDequeueable(frb);
}
戻り値がint
型なのはC言語(C89)にbool型が存在しないためです。本当はbool型で成否を返したいですが、仕方ないのでint型で定義しています。これらAPIもインスタンスの内容を操作しないのでconst*
で引数を定義します。
キューAPI
Enqueue
/* Enqueue
* 容量オーバ時の結果は未定義
*/
void FixedSizeRingBuffer_Enqueue(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
uint32_t rounded_wp = frb->wp_ % frb->max_elements_count_;
frb->buffer_for_elements_[rounded_wp] = element;
frb->wp_++;
return;
}
wp_
は増え続けるのでバッファ範囲をオーバーします。そこで、wp_
をmax_elements_count_
で割った余り(剰余)でバッファにアクセスします。
Dequeue
/* Dequeue
* 積荷が無い場合の結果は未定義
*/
FixedSizeRingBufferElement FixedSizeRingBuffer_Dequeue(FixedSizeRingBuffer* frb)
{
FixedSizeRingBufferElement element;
element = frb->buffer_for_elements_[frb->rp_];
frb->rp_++;
if (frb->max_elements_count_ <= frb->rp_) {
frb->rp_ -= frb->max_elements_count_;
frb->wp_ -= frb->max_elements_count_;
}
return element;
}
積荷を取り出した後、rp_
を進めます。rp_
がバッファ範囲をオーバーするタイミングでrp_
とwp_
の両方からバッファサイズを引きます。rp_
≦wp_
が常に成り立つので、この操作によりrp_
のアクセス範囲はバッファ範囲内に収まり続け、wp_
も増え続けずに一定の範囲を巡回します。
スタックAPI
Push
Enqueueと同じです。
Pop
Dequeueとは異なり、キューの末尾から値を取り出す必要があります。
/* Pop
* 積荷が無い場合の結果は未定義
*/
FixedSizeRingBufferElement FixedSizeRingBuffer_Pop(FixedSizeRingBuffer* frb)
{
uint32_t rounded_wp;
frb->wp_--;
rounded_wp = frb->wp_ % frb->max_elements_count_;
return frb->buffer_for_elements_[rounded_wp];
}
要はEnqueueの逆操作なので、wp_
を戻してからバッファの中身を取り出して返します。バッファアクセスは剰余結果で行う必要があります。
チェックを伴うAPI
基本的に、積荷操作系APIを呼び出す前にチェックAPIで事前にチェックせよ、という設計思想です。なので完全に蛇足ですが、チェックAPIと積荷操作系APIを組み合わせて、成否を返す積荷操作系APIも用意しています。単にチェック系APIで事前チェックし、実行可能であれば積荷操作系APIを実行するという実装になっています。
/* チェックありEnqueue
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_EnqueueWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
if (!FixedSizeRingBuffer_IsEnqueueable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
FixedSizeRingBuffer_Enqueue(frb, element);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
/* チェックありPush
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_PushWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement element)
{
if (!FixedSizeRingBuffer_IsPushable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
FixedSizeRingBuffer_Push(frb, element);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
/* チェックありDequeue
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_DequeueWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement* dequeued_element)
{
if (!FixedSizeRingBuffer_IsDequeueable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
*dequeued_element = FixedSizeRingBuffer_Dequeue(frb);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
/* チェックありPop
* 成功時は0以外、失敗時は0を返す
*/
int FixedSizeRingBuffer_PopWithCheck(
FixedSizeRingBuffer* frb,
FixedSizeRingBufferElement* popped_element)
{
if (!FixedSizeRingBuffer_IsPoppable(frb)) {
return FIXEDSIZERINGBUFFER_LOCAL_FALSE;
}
*popped_element = FixedSizeRingBuffer_Pop(frb);
return FIXEDSIZERINGBUFFER_LOCAL_TRUE;
}
C言語はタプルを返せないので、目的とする値と成否を同時に返す場合に以下の選択肢があります。
- 目的の値に不正値を定義し、戻り値として返す(負値やNULLの場合は失敗として扱う流儀)
- 戻り値を成否判定値とし、引数に出力値用のポインタを受け取る
- 成否と目的の値をメンバに持つ構造体で返す
1.の方法は目的の値に不正値を定義する余地があれば可能ですが、今回のリングバッファの仕様上、FixedSizeRingBufferElement
の有効な値の範囲によっては実装が破綻するので採用できません。必ず不正値を表現できる値のみ扱うという制約を課せばできなくはないです。その場合はインスタンス構築時に不正値を指定するようなインタフェース にすると良いかもしれません。
3.の方法はユーザーコード側で使いづらいのと、構造体を返す関数に拒否反応を示すプログラマがいてボロクソに非難してくるので避けます。
というわけで消去法で2.を採用しています。2.の方法は2.の方法で、出力値用のポインタ引数に不正なポインタを指定されると死ぬというデメリットもありますが、その時は仕方ないので諦めます。全体的に不正なポインタを渡す方が悪いです。NULLポインタもここでは不正なポインタです。また、戻り値で目的の値を返してもらえないので、式の一部として記述する事ができず、これもユーザーコード側では使いづらいです。
というかこれらの問題を避ける意味で、事前チェック用のAPIを用意しています。そもそも実行しないと成否判定できないというのも、例えば複数のキューやスタックを操作する際の例外安全的な設計からすると厳しい(副作用の巻き戻しを実装する必要がある)ので、原則は事前チェック用APIを用意する事が望ましいです。
その他
rp_
の値の範囲は0
〜max_elements_count_
の範囲ですが、wp_
の範囲はmax_elements_count_ * 2 - 1
に成り得ます。max_elements_count_
はインスタンス構築時にUINT32_MAX
を指定可能なので、何もしなければオーバーフローが発生して期待した動作をしません。
一つの解決方法は、インスタンス構築時のmax_elements_count
の型をuint16_t
にしてしまう事です。扱える要素数が最大UINT16_MAX
個に制限されてしまいますので、その程度の要素数しか扱えなくても問題なければですが。
また、ポインタ引数のNULL
チェックを行なっていませんが、それはNULL
を渡す方が悪いという事にしています。実行時にNULL
チェックしてエラーを返すようにした場合、それに伴うエラーハンドリングがユーザーコード側に必要です。そこでエラーハンドリングするのであれば、そもそも不正なポインタを渡さないようにユーザーコード側でハンドリング可能です。NULL
だけが不正なポインタというわけでもないので、この辺りのチェックは不毛な事が多いです。それでもソフトウェアが大規模化してきて正統性の保証が難しくなってきた場合、問題の原因特定のためにチェックコードを埋め込んでおく事が有用な場合もありますが、実装についての本質的な要求ではないと考えるのでここでは対応しません。
おわりに
C言語でソフトウェアを作っていると、何度もこの手の車輪の再開発が必要になるので不毛ですね。しかも関係するプログラマの構成によっては実装や設計に度々疑義が入るというつらさもあります。できればコピペで済ませたいところです。