はじめに
未だに多くの人が、printf を使い続けています。
基本的に、可変引数の場合、パラメーターの受け渡しをスタックで行う為、printf 内のフォームと、不整合のあるパラメーターを指定すると、最悪の場合プログラムがクラッシュします。
コンパイラは、printf のフォームと、引数の不整合が無いかを検査しますが、完全に検査する事は出来ません。
これらの事から、多くの開発現場では、printf の利用を禁止するルールが運用されています。
#include <stdio.h>
#define printf(fmt, ...)
※「printf」を消す。
C++ では、printf に代わる機能として、iostream クラスがあり、文字列の表示を行う事が出来ます。
しかし、iostream クラスを使って文字列を扱った場合、printf にあるようなコンビニエンス性が無く、慣れの問題もあり敬遠されがちだと思えます。
boost では、これらの事情を考慮して、printf に限りなく近く使えて、「安全」 な boost::format クラスが実装されています。
※ boost::format クラスでは、最終的な文字出力は、iostream を使っています。
組み込み開発では、別の問題があります。
通常、組み込みマイコンのような小規模システムでは、iostream 関連(std ライブラリ)を使う事で多くのメモリを消費してしまい、現実的に使う事が困難です。
iostream におけるメモリ消費
以下は、RXマイコン、g++ による例です。
std::cout << "Hello!" << std::endl;
text data bss dec hex filename
506976 47556 8796 563328 89880 test_test.elf
---
utils::format("Hello!\n");
text data bss dec hex filename
5120 48 1860 7028 1b74 test_test.elf
上記のように「iostream」を使った場合と、自前「format」を使った場合の実行バイナリの違いは明らかです。
printf が文字出力する仕組み
まず、システムにおいて、printf が文字を出力する仕組みを「おさらい」します。
printf は、文字出力として、putchar 関数を呼び出しています。
putchar 関数は、標準出力「stdout」に対して、ファイル書き込みに相当する関数を呼び出します。
従って、write 関数をオーバーライドすれば、printf で文字出力した先を自由に操作する事が出来ます。
write 関数を呼び出す際、ファイルハンドルとして、「stdout」を使います。
※通常、「stdout」は、「1」番が使われます、「stderr」は「2」番。
write 関数など、ファイル操作関係の関数は、特殊な属性で、gcc では以下のように定義されています。
ssize_t write(int fd, const void* src, size_t len) __attribute__((weak));
「weak」は、リンクの際、実態があれば、その関数がリンクされます。
この仕様の為、アプリ側で、write を実装して、たとえば、シリアル出力に送れば、printf した場合に、最終的に文字列はシリアル出力されます。
_READ_WRITE_RETURN_TYPE write(int file, const void *ptr, size_t len)
{
if(ptr == NULL) return 0;
_READ_WRITE_RETURN_TYPE l = -1;
if(file >= 0 && file <= 2) {
if(file == 1 || file == 2) {
const char *p = ptr;
for(int i = 0; i < len; ++i) {
char ch = *p++;
sci_putch(ch);
}
l = len;
errno = 0;
}
}
...
return l;
}
自前の「format」クラスの使い方
C言語の時代、組み込み開発では、libc の printf が巨大な為、自前の printf を実装していました。
これは、float を使わないとか、カスタマイズでき、適度な規模であった事などが理由と思います。
それに習って、boost::format クラスに代わる、format クラスを実装しています。
※車輪の再発明となっていますが、カスタマイズする事が出来る為、多少の意味はあると考えます。
例えば、以下のように使えます。
int value = 123;
utils::format("Value=%d\n") % value;
C++ では、オペレーター機能があり、この場合、算術の「%」を別の機能として利用する事が出来ます。
可変引数を使っていないので、不整合のある指定を行ったとしても、スタックが壊れる事もなく安全です。
boost::format では、不整合のある引数を指定した場合、「例外」がスローされますが、組み込みでの利便性を考えて、内部エラーコードをサービスします。
float a = 1.0f;
auto err = (utils::format("Fail int: %d\n") % a).get_error();
utils::format("Error: %d\n") % static_cast<int>(err);
冗長ですが、エラーコードを取得して検査する事も出来ます。
enum class error : uint8_t {
none, ///< エラー無し
null, ///< 無効なポインター
unknown, ///< 不明な「型」
different, ///< 異なる「型」
};
通常、stdout に出力されますが、文字列を作りたい場合は、以下のようにします。
using namespace utils;
float x = 1.0f;
float y = 2.0f;
float z = 3.0f;
char tmp[512];
sformat("Value: %f, %f, %f\n", tmp, sizeof(tmp)) % x % y % z;
sformat("Value: %f, %f, %f\n", tmp, sizeof(tmp), true) % y % z % x;
sformat("Value: %f, %f, %f\n", tmp, sizeof(tmp), true) % z % x % y;
int size = sformat::chaout().size();
format("(%d)\n%s") % size % tmp;
Value: 1.000000, 2.000000, 3.000000
Value: 2.000000, 3.000000, 1.000000
Value: 3.000000, 1.000000, 2.000000
※上記の例では、文字バッファに「追記」もしています。(「true」)
組み込みマイコンでは、A/D変換された整数値など、固定小数点で表示する場合が多いので、それをサポートしています。
{ // 固定小数点
uint16_t val = (1 << 10) + 500;
format("Fixed point: %4.3:10y\n") % val;
}
Fixed point: 1.488
上記の例では、小数点以下10ビット、表示は全4桁、小数点以下3桁の場合です。
現在、制限事項として、倍精度浮動小数点の表示を完全にサポートしていません。
※printf では、内部の扱いは「double」が基準で、「float」は「double」にキャストされています。
また、浮動小数点表示も、printf の場合と異なるかもしれません、これは十分なテストがされていない為です。
また、IEEE754 浮動小数点フォーマットのデコードを独自に行っている事も理由です、それでも、通常の表示は問題無いと思います。
※最初、printf の実装を参考にしようと思いましたが、あまりに複雑なので、再利用する事が難しいと判断した為です。
※内部処理では、float の演算を一切使用していません、全て整数だけで処理しています、これは、float 関連をエミュレーションで処理するマイコンではライブラリをリンクする必要性があり、実行バイナリーが巨大になる為です。
「format.hpp」で全て簡潔しています、RXマイコン以外のシステムでも、「format.hpp」をインクルードするだけで使えると思います。
浮動小数点を必要としない場合は、「format.hpp」をインクルードする前に、以下のようにすれば、多少メモリを節約する事が出来ます。
※これはリソースが少ない 8/16 ビットマイコンには有用です。
※リソースの節約に主眼が置かれている為、スピードはあまり速くありません。
#define NO_FLOAT_FORM
#include "format.hpp"
ソースコードは、
https://github.com/hirakuni45/format_class
にあります。