mruby/cの表示ルーチンの改良
mruby/cには,表示をするやり方が複数ある. ruby側からは,p,puts,print,printfなどがあり, C言語からもいくつもの表示方法がある. ruby側から表示する関数は,c_object.cの中で定義されているが, p,puts,printでは,対応するC言語側の関数を呼び出しているのに対して, printfはsprintfを通して,mrbc_printf_intやmrbc_printf_floatを呼び出している. C言語から表示する関数は,console.cの中で定義されており, mrbc_print,mrbc_printfなどを使って表示することになっているが, 整数の場合にはmrbc_printf_intで, 浮動小数の場合にはmrbc_printf_floatとmrbc_format_floatで整形されている.
整数を扱うmrbc_printf_intでは,整数を適切に整形するプログラムが書かれているが,負の数でゼロ埋めをする際の処理に軽微なバグがあることを,2026/6/29のブログで紹介した. 一方,浮動小数を扱うmrbc_printf_floatとmrbc_format_floatでは, 共に整形の部分はsnprintfに丸投げしている. この関数は,非常にコンパイルサイズが大きく,mruby/cにFloatを組み込んだときに大きくなる一つの要因になっていると思われる. 小数はC言語のライブラリを使って整形しているのに,整数はmruby/cでコード書いているのは,Floatを無効にしたときに,整数を整形できるようにするためであろうが,Floatを使う際には整数の整形ルーチンがダブってしまい,無駄が多い.
そこで,小数の整形をするルーチンを独自に書き下して,snprintfを使わないようにすれば,Floatを組み込む際にも,それほど大きくならないように出来るかも知れない. また,ch32funのsnprintfでは小数は表示できないので,Floatの表示ができないが,その問題も解消できる. mrbc_format_floatは,value.cの中で定義されているが, その中のsnprintfはmrbc_snprintfに変更することができる. console.cの中で定義されているmrbc_printf_floatは, mrbc_snprintfで使われているので, 元の関数はmrbc_format_float2と名前を変えて, mrbc_printf_floatを新たに定義することにした.
Floatの表示は,通常の小数表記,科学的な表記,自動判定の3つの方法があり,これらはそれぞれ’f’,’e’,‘g’で表され,大文字を使う’E’と’G’もある. これらの表示をするためには,どのような処理が必要かを考えると,整数部と小数点以下を分離して桁数などに応じての表示と,指数部と仮数部の計算などをしなければならないことが分かる. 指数部は,自動判定時の小数表記と科学的表記の分岐にも使われる. そこで,通常の小数を表示する関数と,指数部と仮数部を計算してそのまま科学的表記をするか先の関数を呼び出して小数の表示をする関数の2つの関数を定義し,mrbc_format_floatからは指定に応じてこれらの2つの関数を呼び出すことにした.
通常の小数を表示する関数では,絶対値に直してから,その整数部分と精度を考慮した小数部を,整数として求めて,小数点を挟んでそれらをそれぞれmrbc_printf_intを用いて表示すれば良い. 前者は必要なら符号も表示し,後者は精度の幅でゼロ埋めして表示する. この関数は,科学的な表記の仮数部分を表示するときに呼び出すが,四捨五入で繰り上がりが生じて桁が変わる場合には,仮数を桁を調整しなければならないので,科学表記のときにはそのための処理も行うようにした. この関数を定義するときに,できるだけサイズを小さくするために,効果が大きな2つの工夫を行った. 1つ目の工夫は整数部と小数部に分ける際に,小数点の位置を精度の分だけ下にずらしてから四捨五入のために0.5を足した後で,うまくその2つの部分を取り出したことである. 単精度のときには,全体を整数に変換して,その上位の整数部と下位の小数部を割り算を用いて計算すると,32bitのCPUで扱いやすく,サイズを抑えることができた. 倍精度のときには,整数部は小数点の位置を戻して整数化して取り出し,小数部は整数部の桁数をずらして引くことによって取り出すと,小さくすることができた. 両者で小さくなるようなアルゴリズムが違うのは奇妙な気もする. しかしこのやり方では,整数部や精度が大き過ぎる場合には,問題が起こる可能性もあることに注意が必要である. 2つ目の工夫は,符号と絶対値を求める際に,専用の関数を使ったことである. 単にx<0や-xを計算するだけなら,バイナリでも単純になると思っていたのだが,doubleでは複雑な処理が行われるらしく,math.hを組み込んで,signbitとfabsを使うことで,100バイト以上小さくなった.
指数部の計算などをする関数では,絶対値に直してから,仮数xが1<=x<10になるまで,10を掛けたり割ったりして,同時に指数部を数える. そして,表示方法を自動判定するときには,精度と指数から判定して,小数表記なら先の関数を呼び出して,科学表記なら仮数を同様に表示して指数部は整数として表示する. この関数でも,doubleの処理の部分を工夫することが重要である. 絶対値を取る部分は,先と同様にも処理できるが,1.0や10.0との比較する部分を小さくするために,doubleをuint64_tとして解釈して,処理することにした. 正の数の場合には,doubleの大小とuint64_tの大小が一致することを利用して,大小を判定した方が,コンパイルサイズが小さくなったので,そのようにすることにした. それに伴って,絶対値や符号の処理はbit演算で表した. ソースの可読性が下がるという欠点はあるが,コンパクトにするためには仕方ないだろう.
これで,浮動小数点も表示できるようになったが, 精度を指定しなかったときには,それが0になるようになっており, そうすると,printf “%f”,3.14としても,3しか表示さらなくなってしまった. 精度が0のときには,6にするようにすると,今度は精度を0にできなくなってしまう. そこで,精度を指定しなかったときの値を-1にした. そのために,関数mrbc_printf_mainの中でpf->fmtが初期化された直後に-1を代入する.
pf->fmt.precision=-1;
そして,ピリオドがあったら,その値を0にして,精度を取り込む.
pf->fmt.precision=0;
しかし, pf->fmt.precisionが0で無い場合に,精度が定義されているという判断をしている箇所が, mrbc_printf_bstrとmrbc_printf_intの中に見つかったので, それらの条件を以下のように書き換えた.
pf->fmt.precision > 0
この修正により,デフォルトの表示の精度の問題も無くなり,思った通りに表示できるようになった. snprintfを使っていた場合には,フォーマットの文字列もこの関数で処理されるので,この問題は生じていなかったのである.
console.cにこれらの修正をした後に,その末尾に先に考えた以下のコードを追加すれば, 新たなmrbc_print_floatが定義される.
#if MRBC_USE_FLOAT
#include <math.h>
static void mrbc_printf_f(mrbc_printf_t *pf, double value, int *exponent) {
int is_negative = signbit(value);
value=fabs(value);
int precision = pf->fmt.precision;
#if MRBC_USE_FLOAT == 1
if (precision > 9) precision = 9;
uint32_t scale = 1;
#elif MRBC_USE_FLOAT == 2
if (precision > 16) precision = 16;
uint64_t scale = 1;
#endif
for (int i = 0; i < precision; i++) scale *= 10;
value = value * (double) scale + 0.5;
#if MRBC_USE_FLOAT == 1
uint32_t total_int = (uint32_t)value;
int32_t int_part = total_int / scale;
uint32_t frac_part = total_int % scale;
#elif MRBC_USE_FLOAT == 2
int64_t int_part = (uint64_t) (value / scale);
uint64_t frac_part = (uint64_t) (value - int_part*scale);
#endif
if (exponent && int_part >= 10) {
(*exponent)++;
int_part = 1;
frac_part = 0;
}
if(is_negative) int_part = -int_part;
pf->fmt.width -= precision ? precision+1 : 0;
pf->fmt.precision = 0;
mrbc_printf_int(pf, (mrbc_int_t)int_part, 10);
if (precision > 0) {
if (pf->p < pf->buf_end) *pf->p++ = '.';
pf->fmt.width = precision;
pf->fmt.flag_plus = 0;
pf->fmt.flag_minus = 0;
pf->fmt.flag_zero = 1;
mrbc_printf_int(pf, (mrbc_int_t)frac_part, 10);
}
}
void mrbc_printf_e(mrbc_printf_t *pf, double value, int is_general) {
union { double d; uint64_t u64; } cast;
//cast.d = fabs(value);
cast.d = value;
uint64_t sign_bit = cast.u64 & 0x8000000000000000ULL;
cast.u64 &= 0x7FFFFFFFFFFFFFFFULL; //abs
int exponent = 0;
if (cast.u64 != 0) {
while (cast.u64 >= 0x4024000000000000ULL) { //10.0
cast.d /= 10.0;
exponent++;
}
while (cast.u64 < 0x3FF0000000000000ULL) { //1.0
cast.d *= 10.0;
exponent--;
}
}
//cast.d=copysign(cast.d,value);
cast.u64 |= sign_bit;
int precision = pf->fmt.precision;
int is_scientific = 1;
if(is_general){
if (precision == 0) precision = 1;
is_scientific = (exponent < -4 || exponent >= precision);
precision--;
}
if (!is_scientific) {
if(is_general) precision -= exponent;
if (precision < 0) precision = 0;
pf->fmt.precision = precision;
mrbc_printf_f(pf, value, 0);
} else {
pf->fmt.precision = precision;
pf->fmt.width -= 4;
mrbc_printf_f(pf, cast.d, &exponent);
if (pf->p < pf->buf_end) *pf->p++ = pf->fmt.type & ~2 ; // e, E
pf->fmt.width = 3;
pf->fmt.flag_zero = 1;
pf->fmt.precision = 0;
pf->fmt.flag_plus = 1;
mrbc_printf_int(pf, (mrbc_int_t)exponent, 10);
}
}
int mrbc_printf_float( mrbc_printf_t *pf, double value ){
pf->fmt.precision = (pf->fmt.precision >= 0) ? pf->fmt.precision : 6;
int saved_width = pf->fmt.width;
int saved_precision = pf->fmt.precision;
char type = pf->fmt.type | 0x20; // lower case
if(type=='f'){
mrbc_printf_f(pf, value, 0);
} else { // e, g
mrbc_printf_e(pf, value, type & 2);
}
pf->fmt.width = saved_width;
pf->fmt.precision = saved_precision;
return -(pf->p == pf->buf_end);
}
#endif
ここで説明した方法により, 外部のフォーマット変換ルーチンに頼ること無く, ほぼmruby/cのルーチンのみで小数を表示できるようになった. 具体的な値は確認していないが,snprintfを使わなくできた場合には,サイズはかなり小さくなっているはずである. Floatを組み込むことが難しかった低性能のマイコンにも,組み込むことができる可能性が高くなったのでは無いかと思う.