C での文字列領域の確保

前置き(長い)

現在開発中の機材の不具合を分析していたところ MCU ではなく PC 側のソフトがよくないと判断をした. 問題の機能を修正したところ、プログラム終了時に segmentation error が出るようになった. その理由は memory leak であり、malloc はしたのに free を忘れているとか、free を多重にやってしまうことだ. つまり潜在的なバグが表に出てきてしまった.

文字列領域の確保と可変長配列 (C99 VLA)

自分のソースを分析したところ malloc で多いものは下記だった.

  • 可変長となる文字列領域の確保
  • クラスもどきの初期化と終了

文字列に関しては可変長であることから、1つの関数内で malloc-free が完結し有効期間が短い. クラスもどきは初期化と終了で行うもので有効期間が長い.

void func(int a, int b, int c)
{
	static const char format[] = "%d...%d %d\n";
	int n = snprintf(NULL, 0, format, a, b, c) + 1;
	char *buf = malloc(n);
	snprintf(buf, n, format, a, b, c);
	hoge(buf);
	free(buf);
}

これを可変長配列に変える.

void func(int a, int b, int c)
{
	static const char format[] = "%d...%d %d\n";
	int n = snprintf(NULL, 0, format, a, b, c) + 1;
	char buf[n];
	snprintf(buf, n, format, a, b, c);
	hoge(buf);
}

可変長配列について、実際にアセンブラレベルでどういう処理をしているのかは知らないので個人的には MCU 用に使う気はないのだが、性能が高い PC であれば気にはならない(と思い込んでいる). それよりも free を書かなくていいことは気分が楽になる.

上の例では malloc-free が1つの関数内であるので忘れにくいが、実際には文字列生成関数の中で malloc をして生成済みデータのポインタを返す例がかなりあって、その処理では free を忘れていた. 作った直後は忘れることはないが、後日その関数を使い回すと忘れてしまう.

この例を自分が書いたソースで多数発見した. 仕方ないので文字列生成関数は長さを得るだけと、確保済みの領域に書き込む処理の2つに分けて可変長配列を使う前提に変えた.

関数 main() の引数 argv を UTF-8 にする

Windows の main() の argv の文字コードは default code page となっているので、制限や問題が多く一般配布するようなソフトでは使えない. よって UTF-8 を使うことに仕様を決めた.

#if defined(_WIN32_) || defined(_WIN64_) 
static int utf8_main(int argc, char **argv)
#else
int main(int argc, char **argv)
#endif
{
...
}

#if defined(_WIN32_) || defined(_WIN64_) 
static void argv_wchar_to_utf8(int c, wchar_t **wv, char **v, char *dest)
{
	for(int i = 0; i < c; i++){
		int l = wchar_to_utf8_length_get(wv[i]);
		wchar_to_utf8_set(wv[i], dest, l);
		v[i] = dest;
		dest += l;
	}
}
#include <windows.h>
int main(void)
{
	int c;
	wchar_t **wv = CommandLineToArgvW(GetCommandLineW(), &c);
	int length = 0;
	for(int i = 0; i < c; i++){
		length += wchar_to_utf8_length_get(wv[i]);
	}
	char *v[c], utf8_buffer[length]; //C99 VLA
	argv_wchar_to_utf8(c, wv, v, utf8_buffer);
	const int r = utf8_main(c, v);
	LocalFree(wv);
	return r;
}
#endif
  • GetCommandLineW() について, 関数 main の argv を default code page → wchar_t → UTF-8 にすることも可能だが、無駄なので wchar_t を取得する.
  • CommandLineToArgvW() については結局 LocalFree というものが必要で本題と矛盾している.
  • この2つの関数は wmain() を利用することで不要となるはずだが、 msys2 ではうまく使えなかった.
  • 変数 length, utf8_buffer[] については長さを取得した都度変数宣言をすると scope の都合でうまく実装できないので for ループを2つにわける必要が出る.
  • main() での argv に渡される引数は元々は wchar_t で、utf8_main() の中で内容がファイル名である場合はファイル名を渡す APIUTF-8 を wchar_t にまた戻す、その過程でその逆の変換が多重に発生している. この無駄は今のところみなかったことにしている.

結局、矛盾と無駄は残りつつその中ではマシな実装ということになってしまった.