移植性の高いCプログラム

C言語の規格は結構いい加減で、処理系依存と言うcompilerが勝手に決めてよ い部分が何箇所かあります。そのため、AVRではうまく動くプログラムがH8に もっていくと動かない等といったことが起こりえます。
せっかく書いたプログラムが別のマイコンでは使えないというのも寂しいので、 移植性のよいプログラムを書くための注意点をあげてみましょう。

C言語の基本型について

整数型

処理系依存で一番有名なのは、sizeof(int)でしょう。C言語の 整数型はshort,int,long とありま すが、それぞれ何bitであるかは、compilerが自由に決めて良く、 sizeof(short) <= sizeof(int) <= sizeof(long)であることだ けが規格で決められています。
ですので、shortintlongも 16bitという処理系もあれば、shortが16bit、int が32bit、longが64bitという処理系もあります。
例えば、RS-232Cで繋いだマイコンに整数型を出力するつもりで、以下のよう なプログラムを書いたとしましょう。PCもマイコンもintが 32bitなら良いのですが、PC側が32bitでマイコンが16bitだったりすると、正 しくは動きません。

write(tty, &i, sizeof(int));

この問題を回避するため、C99以降ではstdint.hが導入されています。bit長が 重要な処理では、int等を使う代わりに、stdint.hをinclude し て、int16_tint32_tなどを使いましょう。

pointer

整数型の大きさすら決まっていないのですから、当然 sizeof(void*)のようなpointer型の大きさも処理系依存です。 そのため、以下のようなpointerを整数型に代入するような処理は移植性を著 しく下げます。sizeof(int)=2, sizeof(void*)=4 だったり、sizeof(int)=4, sizeof(void*)=8とな る場合、どうなるか考えてみてください。

int i;
void* p;

i=(int)p;

char

型関係で地味なところではsigned charunsigned charの話もあります。char型はsignedunsignedか決まっていません。
charunsigned charとして扱われる系だと、以 下のようなプログラムはまともに動きません。8bitの整数型が必要なら、 int8_tuint8_tを使うようにしましょう。

int test_char(char c)
{
  if( c < 0 ){
    return -1;
  }else{
    return 1;
  }
}

構造体のpadding

基本型に続いて構造体での注意を考えてみます。以下のようなプログラムを書 いたとしましょう。意図する通りに動くでしょうか?

typedef struct{
  uint8_t type;
  uint16_t size;
}DATA_HEADER;

void send_header(int tty, DATA_HEADER* hdr){
{
  write(tty, hdr, sizeof(DATA_HEADER));
}

大抵の系でsizeof(DATA_HEADER)が3にはなりません。4になった り8になったりします。typesizeの間に compilerが勝手に1 byteや3 byteのつめものをしてくれます。
なぜこんなことをするかというと、偶数アドレスへの読み書きが奇数アドレス への読み書きより速いCPU があり、構造体の中の変数も積極的に偶数アドレス に配置されるようにcompilerが気を利かせてくれるのためです。もちろん4の 倍数アドレスが速いCPUや8の倍数アドレスが速いCPUもあるので、構造体のど こに何byteのつめものがされるかはCPUとcompiler次第となります。
対策としては、構造体をまとめてread()write()で入出力しないようにするしかありません。面倒くさい ですが、各メンバ変数ごとに入出力しましょう。

整数型の並び順

続いて、整数型の並び順の話題です。次のようなプログラムを作ると u16には何が入るでしょう?

uint8_t u8[2];
uint16_t u16;

u8[0] = 0x12;
u8[1] = 0x34;

u16 = *( (uint16_t*)u8 );

これも処理系依存で、u16は0x1234か0x3412のどちらかになりま す。基本的にCPUの仕様でどちらか決まっていて、0x1234になる系をbig endian、0x3412になる系をlittle endianと呼びます。
big endianのCPUとしては、Motorolaの68kやRenesasのH8などが、little endianのCPUとしてはIntelのx86があります。PowerPCやMIPS、SHなどは設定の 切替えでbig endianでもlittle endianでも動くようになっています。

並び順問題でうっかりやってしまいがちな例を出してみます。RS-232Cへ16bit 送信するプログラムですが、処理系によって0x12、0x34の順で送信されるか 0x34、0x12の順で送信されるかわかりません。

uint16_t u16;

u16 = 0x1234;
write(tty, &u16, 2);

処理系非依存にするため、以下のように1byteごとに変換してから送受信する ようにしましょう。

void send16(int tty, uint16_t u16)
{
  uint8_t u8[2];

  u8[0] = u16 >> 8;
  u8[1] = u16 & 0xff;
  write(tty, u8, 2);
}

uint16_t recv16(int tty)
{
  uint16_t u16;
  uint8_t u8[2];

  read(tty, u8, 2);
  u16 = ( (uint16_t)u8[0] << 8 ) | u8[1];
  return u16;
}

Bit field

整数型の並び順と同様に、構造体のbit fieldの順番も処理系依存になります。

typedef union{
  uint8_t u8;
  struct{
    uint8_t b0:1;
    uint8_t b1:1;
    uint8_t b2:1;
    uint8_t b3:1;
    uint8_t b4:1;
    uint8_t b5:1;
    uint8_t b6:1;
    uint8_t b7:1;
  };
}UINT8_BITS;

上記のような共用体を用意した場合、
u8 = b0 + b1*2 + b2*4 + b3*8 + b4*16 + b5*32 + b6*64 + b7*128
となる処理系と
u8 = b7 + b6*2 + b5*4 + b4*8 + b3*16 + b2*32 + b1*64 + b0*128
となる処理系があります。

移植を前提にするならbit fieldは使わずshift、and、or演算の組合せで書き ましょう。単純なbitのon/offなら以下のように書けます。

u8 &= 0xf7;  /* b3=0 */
u8 |= 0x20;  /* b5=1 */

割算の商と余り

最後に割算の話題です。C言語ではなんと四則演算すら処理系依存なのです。

q1 = 7/3;
r1 = 7%3;

q2 = (-7)/3;
r2 = (-7)%3;

この例では(q1, r1)=(2,1)なのは処理系非依存で す。問題はq2r2でして、(-7) = (-3)*3+2とも (-7) = (-2)*3-1ともとれます。Cでは商が負の値になった場合、どちらに切り 捨てるか決まっていなくて、(q2,r2)の組合せが (-3,2)になる処理系と(-2,-1)となる処理系があります。

この問題を避けるため、stdlib.hにdiv()という関数があって、 この関数を使うとどの処理系でも (q2,r2)=(-2,-1)となる演算を行えます。 (商の絶対値が小さくなるよう切り捨てる)

C99以降では、割算演算子の"/"は処理系非依存になっ て、div()関数と同等の演算をするように規格化されています。


ご意見、ご感想は、花房 真広 <webmaster@hanabusa.net>まで。メールする前にtop pageの注意書を読んでください。