海外の組み込みコンパイラの注意点

組込みの開発統合環境としては、ルネサスエレクトロニクスからはCubeSuiteやTexas InstrumentsからはCode Composer Studio、Analog DevicesからはVisual DSP、IARからはEmbedded Work Bench for ARMなどなど、各マイコン・各DSPに対応した統合環境およびコンパイラがあります。この中で海外のコンパイラを使う場合にはちょっと注意が必要です。

多言語対応と言えばUnicodeですが、ソースコードはUnicode保存形式をデフォルトとするのは少ないです。そのようなコンパイラを日本語環境で扱うと使うのはMBCS、具体的にはShift-JISになります。このShift-JISは日本含めアジアの数カ国で使うだけなのでTIやアナデバなどのアメリカから見れば「何それ?」ってなるわけです。それが原因で問題になるソース例はこれ。

Test.c

double AreaOfCircle(double radius)
{
    const double pi = 3.141592;
    const double minRad = 0.1;
    double result;
    if (radius < 0)
    {
        result = minRad * minRad * pi;
        // 負値で返すべきは上位が認識可能な分解能
    }else{
        result = radius * radius * pi;
    }
    return result;
}

このコメント中、’能’はShit-JISでは0x94, 0x5cです。末尾の0x5cはASCIIでいうと ‘¥’ 。C言語的には’¥’はプリプロセサマクロで文字の連結を表します。つまりコメント末尾の文字に ‘¥’があると、その文字がShit-JISと判断できない限り、次の文字列と連結します。そのため上のソースはShift-JIS対応していないコンパイラには以下のように見えます。

Test.c(コンパイラが認識するソース)

double AreaOfCircle(double radius)
{
    const double pi = 3.141592;
    const double minRad = 0.1;
    double result;
    if (radius < 0)
    {
        result = minRad * minRad * pi;
        // 負値で返すべきは上位が認識可能な分解能    }else{
        result = radius * radius * pi;
    }
    return result;
}

よって結果がおかしくなります。そこで組み込み用に書いたソースコードの中で、上記のような問題になる文字が無いかをチェックするツールを用意してみましょう。ルートディレクトリを指定して、それ以下の対象ソースを再帰的に検索し、該当ソースファイルとその行数を出力します。

 Program.cs

namespace ShiftJISFileChecker
{
    using System;
    using System.IO;

    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 1)
            {
                ShowHelp(Console.Out);
                return;
            }
            Search0x5cFromFolder(Console.Out, args[0]);
        }

        private static void ShowHelp(TextWriter writer)
        {
            writer.WriteLine(@"使い方:");
            writer.WriteLine(@"コマンドラインからプログラム引数に捜索対象のディレクトリを指定します。");
            writer.WriteLine(@"指定されたフォルダを再帰的に検索し、コメント末尾に0x5Cが含まれている");
            writer.WriteLine("ファイルを出力します。");
            writer.WriteLine();
            writer.WriteLine(@"利用例:SearchJISFileChecker.exe C:\");
            writer.WriteLine(@"出力例:1件見つかりました。");
            writer.WriteLine(@"    C:\work\test.c L.20");
        }

        private static void Search0x5cFromFolder(TextWriter writer, string rootDirectory)
        {
            try
            {
                var searcher = new SearchFolderHierarchcally();
                var result = searcher.Examine(rootDirectory, Search0x5cFromFile.GetFileLines);
                if (result.Count == 0)
                {
                    writer.WriteLine("Shift-JIS問題の懸念となるソースファイルは見つかりませんでした。");
                    return;
                }
                writer.WriteLine("{0}件見つかりました。", result.Count);
                foreach (var item in result)
                {
                    writer.WriteLine(item);
                }
            }
            catch (ArgumentException e)
            {
                writer.WriteLine(e.Message);
            }
        }

    }
}

 

SearchFolderHierarchcally.cs

namespace ShiftJISFileChecker
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.IO;

    internal class SearchFolderHierarchcally
    {
        private List<string> targetExtensions = new List<string>(){
            ".c",
            ".cpp",
            ".h"
        };
        internal IList<string> Examine(string root ,Func<string, IList<string>> examineFunc){
            if (!Directory.Exists(root)){
                throw new ArgumentException("指定されたディレクトリが存在しません");
            }
            var directoryInfo = new DirectoryInfo(root);
            List<string> result = new List<string>();
            foreach(var file in directoryInfo.GetFiles()){
                string fileName = file.FullName;
                if( targetExtensions.Contains(Path.GetExtension(fileName),
                    StringComparer.InvariantCultureIgnoreCase)){
                    // 1ファイルを捜索 + 対象行数を結果にコピー
                    result.AddRange(examineFunc(fileName));
                }
            }
            foreach(var subDirectory in directoryInfo.GetDirectories()){
                // サブディレクトリを再帰的に検索.
                result.AddRange(Examine(subDirectory.FullName, examineFunc));
            }
            return result;
        }
    }
}

 

Search0x5cFromFile.cs

namespace ShiftJISFileChecker
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.IO;

    internal class Search0x5cFromFile
    {
        internal static string filePath;
        private const string targetChars = "―ソЫⅨ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭";

        internal static IList<string> GetFileLines(string targetFilePath)
        {
            filePath = targetFilePath;
            IList<string> result = new List<string>();
            using (StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("Shift-JIS")))
            {
                ReadAndGet0x5c(reader, result);
            }
            return result;
        }

        private static void ReadAndGet0x5c(StreamReader reader, IList<string> result)
        {
            uint lineCount = 0;
            while (!reader.EndOfStream)
            {
                lineCount++;
                string line = reader.ReadLine();
                if (!line.Equals(String.Empty) &&
                    targetChars.Contains(line[line.Length - 1]))
                {
                    result.Add(filePath + " L:" + lineCount);
                }
            }
        }
    }
}

ざっつおーる。

test

広告

Cソースで守ってほしいこと。

C言語、CPU命令と対応した非常によくできた言語だと思います。C++, Java, C#と続くプログラミングの王道、”C系”のルーツ。

ただそのC言語、簡単なはずなのにちゃんと使われていない!プログラミングのマナーである、わかりやすい関数名・変数名や、SOLID原則や、getterの中では値のsetはしない(コマンド照会分離原則)…等のどの言語でも当てはまることは除いて、C固有(C++すら除外!)の内容についてC使いにどうしても守ってほしいことを書いてみたいと思います。

守ってほしいこと

  1. .cに対応した.hファイルは.cの先頭でincludeすること
  2. static関数のプロトタイプ宣言は必ず書くこと
  3. 変数の宣言時初期化を意識すること

コードを読むのは学習って言うけど、これらも守られていないコードからは何も学べる気がしない。

 

1..cに対応した.hファイルは.cの先頭でincludeすること

間違い例(waveprocess.c): 

#include "type.h"
#include "common.h"
#include "doubleCalc.h"
#include "customheap.h"
#include "filewrap.h"
#include "wavetypes.h"
#include "waveprocess.h"

// 以下の関数は外部公開externされている前提
void DoSomething(profile* waveData){
    ...
}

理由:

DoSomething()を呼び出したい人はwaveprocess.hをインクルードしますよね?(includeせずextern宣言で利用できるけどそんなことしたらカオスだよ) そうするとwaveprocess.hをインクルードする。けど上のコードだとそれだけで使える保証がない。少なくとも引数に使っているprofile型の型定義が参照できなければビルドできないけどそれがはどこで定義されているかわかりませんよね、呼び出し側はそれを探してその定義があるファイルもインクルードする必要がある、しかもそれはwaveprocess.hの前にインクルードしなきゃいけない。外部公開関数が必要とする型はどうせインクルードしないといけないのだから、そんなものはヘッダ側で用意しましょう。それを保証するために.cに対応する.hを.cの先頭でインクルードする、のです。先頭でincludeしたときに外部公開に必要な型定義がなければエラーとなるため、対応する.hを先頭でインクルードすることが必要なヘッダをインクルードした状態であると保証できるのです。

正しい例(waveprocess.c):

#include "waveprocess.h"
// #include "type.h"  -> 基本型の定義だからwaveprocess.hでインクルードしなきゃいけなかった!
#include "common.h"
#include "doubleCalc.h"
#include "customheap.h"
// #include "wavetypes.h" -> ここにprofile型が定義されていた!
#include "filewrap.h"

// 以下の関数は外部公開externされている前提
void DoSomething(profile* waveData){
    ...
}

(waveprocess.h)

#ifndef WAVE_PROCESS_HEADER
#define WAVE_PROCESS_HEADER
#include "type.h"
#include "wavetypes.h"

void DoSomething(profile* waveData);

#endif

コメントアウトは説明のために書いてるだけで本番コードではもちろん書きませんよ。こうすれば、DoSomething関数を呼び出す側はそれが定義されているwaveprocess.hをインクルードするだけでいいのです。絶対そのほうがいいでしょ?

 

2.static関数のプロトタイプ宣言は必ず書くこと

間違い例(Test.c):

static void SubFunc1(){
    ...
}
static void SubFunc2(){
   ...
}
void ExternFunc(){
    SubFunc1();
    SubFunc2();
}

理由:
第一に、上から下に読むという基本に反している。でも「慣れ」って言うかもしれないからこれにはあまり触れない。第二に、関数の変更時に面倒なことになる。例えば、SubFunc1でSubFunc2を使いたいとしたらどうする?上の例で関数呼び出ししたんじゃそんな関数はないってエラーになる。

正しい例(Test.c):

static void SubFunc1();
static void SubFunc2();

static void SubFunc1(){
    ...
}
static void SubFunc2(){
   ...
}
void ExternFunc(){
    SubFunc1();
    SubFunc2();
}

もう定義順なんて気にしない、させない。

 

3.変数の宣言時初期化を意識すること
C++のソースですら下のような例をみる。

間違い例(Test.c):

void TestFunc(waveprofile* profile, rawwave* wave){
    DWORD dwCount;
    BOOL bFlag;
    if( profile->failed){
        dwCount = 2;
    }else{
        dwCount = 4;
    }
    bFlag = IsPeakExist(wave, dwCount);
    ...
}

理由:

確かにCは変数宣言が先頭にないとダメだけど、初期化漏れ・状態不整合についての意識が欠けていないだろうか。状態不整合ってのは値が代入されるまでの間、存在するけど使えない状態があること。上のコードくらいならまだマシかもしれないけど、変数を何個も使い出したらこれは初期化されてるとかこれはまだとか頭にキャッシュしないといけない。状態不整合をカプセル化するためのオブジェクト指向言語であり、状態不整合が副作用を及ぼすから関数型言語があるんだと思っている、私は。

正しい例(Test.c):

void TestFunc(waveprofile* profile, rawwave* wave){
    DWORD dwCount = (profile->failed)? 2 : 4;
    BOOL bFlag = IsPeakExist(wave, dwCount);
    ...
}

条件? 真時の値 : 偽時の値っていう3項演算子を嫌う人もいるけど、不整合を生まない、宣言時に書ける(ifステートメントは書けない)ってことを考えてメリットないって考えるほうがどうかしてる。他にも安易にstatic変数使うなとか、言いたいことはいっぱいある。こんな内容が常識として扱われたらいいんだけど。・・・グチでした。