文字列のトリミングと範囲の取得

文字列のトリミングは、指定した文字の集合を文字列の左端と右端から取り除く、よく使う操作です。長い文字列から一部分だけを取り出すことも、便利な文字列操作です。

void sdstrim(sds s, const char *cset);
void sdsrange(sds s, int start, int end);

SDSは、sdstrimとsdsrange関数で、この両方の操作を提供します。ただし、両関数は、SDS文字列を変更する大半の関数とは異なり、戻り値はnullです。基本的には、渡されたSDS文字列を常に破壊的に変更し、新しい文字列を確保することはありません。トリミングと範囲の取得はいずれも元の文字列から文字を取り除くだけなので、追加の容量を必要としないためです。

このため、両関数は高速で、再確保を伴いません。

次は、SDS文字列から改行と空白を取り除くトリミングの例です。

sds s = sdsnew("         my string\n\n  ");
sdstrim(s," \n");
printf("-%s-\n",s);

output> -my string-

sdstrimは、第1引数にトリミングするSDS文字列を受け取り、次に、文字列の左端と右端から取り除く文字の集合をNUL終端文字列として受け取ります。指定した集合にない文字が現れるまで、文字を取り除きます。そのため、前の例では"my"と"string"の間の空白が残ります。

範囲の取得も同様ですが、文字の集合ではなく、文字列内の0始まりのインデックスで開始位置と終了位置を指定し、残す範囲を決めます。

sds s = sdsnew("Hello World!");
sdsrange(s,1,4);
printf("-%s-\n");

output> -ello-

インデックスを負数にすると、文字列の末尾から数えた位置を指定できます。-1は最後の文字、-2は最後から2番目の文字を表します。

sds s = sdsnew("Hello World!");
sdsrange(s,6,-1);
printf("-%s-\n");
sdsrange(s,0,-2);
printf("-%s-\n");

output> -World!-
output> -World-

sdsrangeは、プロトコルを処理したり、メッセージを送ったりするネットワークサーバーの実装にとても便利です。たとえば、次のコードは、Redis Clusterのノード間メッセージバスの書き込みハンドラーで使われています。

void clusterWriteHandler(..., int fd, void *privdata, ...) {
    clusterLink *link = (clusterLink*) privdata;
    ssize_t nwritten = write(fd, link->sndbuf, sdslen(link->sndbuf));
    if (nwritten <= 0) {
        /* Error handling... */
    }
    sdsrange(link->sndbuf,nwritten,-1);
    ... more code here ...
}

送信先ノードのソケットが書き込み可能になるたびに、できるだけ多くのバイトを書き込もうとします。そして、sdsrangeを使い、送信済みの部分をバッファーから取り除きます。

クラスター内のノードへ送る新しいメッセージをキューへ入れる関数は、sdscatlenを使って、送信バッファーへデータを追加するだけです。

Redis Clusterのバスはバイナリープロトコルを実装していますが、SDSはバイナリーセーフなので問題ありません。このように、SDSの目的は、Cプログラマーへ高水準の文字列APIを提供することだけではなく、管理しやすい動的確保バッファーを提供することでもあります。

文字列のコピー

C標準ライブラリーで最も危険で悪名高い関数は、おそらくstrcpyでしょう。より良く設計された動的文字列ライブラリーでは、文字列のコピーという概念がほぼ不要になるのは、面白いことかもしれません。通常は、必要な内容を持つ文字列を作成したり、必要に応じて内容を追加したりします。

それでも、SDSには性能が重要なコード部分で役立つ文字列コピー関数があります。ただ、5万行からなるRedisのコードベースでは一度も呼ばれなかったため、実用上の用途は限られているのではないかと思います。

sds sdscpylen(sds s, const char *t, size_t len);
sds sdscpy(sds s, const char *t);

SDSの文字列コピー関数はsdscpylenと呼ばれ、次のように動作します。

s = sdsnew("Hello World!");
s = sdscpylen(s,"Hello Superman!",15);

このように、関数は入力としてSDS文字列sを受け取りますが、SDS文字列を戻り値としても返します。これは文字列を変更する多くのSDS関数に共通しています。戻り値は、変更した元のSDS文字列の場合も、新しく確保した文字列の場合もあります。たとえば、元のSDS文字列に十分な容量がなければ、新しい文字列を確保します。

sdscpylenは、元のSDS文字列の内容を、ポインターと長さの引数で渡した新しいデータへ置き換えるだけです。sdscpyという同様の関数もあり、こちらは長さを必要とせず、代わりにNUL終端文字列を想定します。

既存のSDS文字列へ値をコピーしなくても、新しい値からSDS文字列を作り直せるのに、コピー関数を用意する理由があるのかと思うかもしれません。理由は効率です。sdsnewlenは常に新しい文字列を確保します。一方、sdscpylenは、利用者が指定した新しい内容を保持するだけの容量があれば、既存の文字列の再利用を試み、必要な場合にだけ新しい文字列を確保します。

文字列の引用表現

利用者へ一貫した出力を提供するためや、デバッグのために、バイナリーデータや特殊文字を含む可能性がある文字列を、引用表現へ変換することがよくあります。ここでいう引用表現は、プログラムのソースコードで文字列リテラルに使われる一般的な形式です。現在ではJSONやCSVなどのよく知られたシリアライズ形式にも含まれているため、プログラム内の文字列リテラルを表すという用途を超えています。

引用表現の文字列リテラルの例です。

"\x00Hello World\n"

最初のバイトはゼロバイト、最後のバイトは改行なので、この文字列には英数字以外の文字が2つあります。

SDSはこの目的に連結関数を使い、入力文字列の引用表現を既存の文字列へ連結します。

sds sdscatrepr(sds s, const char *p, size_t len);

scscatreprのreprはrepresentation(表現)の意味です。この関数は、通常のSDS文字列関数の規則に従い、文字ポインターと長さを受け取ります。したがって、SDS文字列、len引数にstrlen()を使った通常のC文字列、バイナリーデータのいずれにも使えます。次は使用例です。

sds s1 = sdsnew("abcd");
sds s2 = sdsempty();
s[1] = 1;
s[2] = 2;
s[3] = '\n';
s2 = sdscatrepr(s2,s1,sdslen(s1));
printf("%s\n", s2);

output> "a\x01\x02\n"

sdscatreprは、次の規則で変換します。

  • \と"は、バックスラッシュでエスケープします。
  • 特殊文字'\n'、'\r'、'\t'、'\a'、'\b'をエスケープします。
  • isprintの検査に通らない、ほかのすべての非表示文字は、\x..形式でエスケープします。これは、バックスラッシュ、x、文字のバイト値を表す2桁の16進数の順に並べた形式です。
  • 関数は常に、先頭と末尾に二重引用符を追加します。

SDSには逆変換を行う関数もあり、後のトークン化の節で説明します。