プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

Javaにlambda修飾子が欲しい

 機能(関数)をクラス内に実装せずに、外部に委譲することがある。単純な例ではこんな感じ。

public class Action{
  private Runnable action;
  public void setAction(Runnable action){this.action = action;}
  public void action(){  if(action != null) action.run(); }
}


 Runnableのように引数を取らないような関数なら別に問題ないが、大概の場合は委譲元のインスタンスを引数にとりたい。
 すると、こうなる。

public class Action{
  private Consumer<Action> action;
  public void setAction(Consumer<Action> action){this.action = action;}
  public void action(){if(action!=null)action.accept(this);}
}




委譲は継承が面倒

 機能の委譲をしている場合でも、データの拡張などでクラスを継承する方が合理的な場合もある。

public class Action{
  private Consumer<Action> action;
  public void setAction(Consumer<Action> action){this.action = action;}
  public void action(){action.accept(this);}
}
class ActionB extends Action{
  public int b;
}

 委譲した関数が、ActionBにしか対応する必要がない場合、このままだと一々キャストが発生する。

actionB.setAction(a->{
  ActionB b = (ActionB)a;//キャストめんどい
  System.out.println(b.b);
})   

 かといって<? extends Action>にしておくのも上手くない。

public class Action{
  private Consumer<? extends Action> action;
  public void setAction(Consumer<? extends Action> action){this.action = action;}
  public void action(){if(action!=null)((Consumer)action).accept(this);}
}
public class ActionB extends Action{
 public int b;
}
public class ActionC extends ActionB{
 public int c;
}


ActionB actionB;
actionB.setAction((ActionB b)->System.out.println(b.b));//ActionBの型を書くのめんどい
actionB.setAction((ActionC c)->System.out.println(c.c));//これも通る





安全だし、ラムダで型を書かなくて良い楽な方法もあるにはある

 ただし、マジでめんどい

public class Action <V extends Action<?>>{
  private Consumer<? super V> action;
  protected void setAction(Consumer<? super V> action){this.action = action;}
  public void action(){if(action!=null)((Consumer)action).accept(this);}
}

class ActionB<V extends ActionB<?>> extends Action<V>{//<V extends ActionB<?>>がめんどい
  int b;
}
class ActionC<V extends ActionC<?>> extends ActionB<V>{//<V extends ActionC<?>>がめんどい
  int c;
}


ActionB<?> actionB = new ActionB<>();//<?>と<>を書くのがめんどい
actionB.setAction(b->System.out.println(b.b));//ラムダ式は楽
actionB.action();




ならば、こう書ければ楽なはずだ

 そもそも人間がガンガるから面倒くさいのだ。コンパイラが頑張れば良いのだ。

 たとえば、以下のようなアノテーションとジェネリックを書いておけば、Setter,Getter,メソッドコールの部分は自動的にJavaコンパイラが頑張ってくれればいいんじゃないのかな。

public class Action{
  @TransferMethod //委譲メソッドであること宣言する的な
  private Consumer<this> action;
}
public class ActionB extends Action{
  public int b;
}

ActionB actionB = new ActionB();
actionB.setAction( b -> System.out.println(b.b) );
actionB.action(); 

 

lambda修飾子があればもっと楽なはずだ

 だが、ここまで考えて気がついたのだが、Consumer<this>も書くのがめんどい
 Consumerならまだ楽だが、引数が増えたときは考えたくない。


 コンパイラが頑張るなら、もはやこれで良いはずだ。

public class Action{
  public lambda void action();
}
public class ActionB extends Action{
  public int b;
}

ActionB actionB = new ActionB();
actionB.action = b -> System.out.println(b.b) ;
actionB.action(); 

 なんと、実に楽だ。ついでにdefaultも使えるとさらに良いかもしれない。

public class Action{
  public default lambda void action(){System.out.println("Action");}
}

 マルチスレッド対応のためにvolatileとかsynchronizedとか修飾できても楽しいかも知れない。


 というわけで、nativeとか、abstractみたいな感じで、lambda修飾子があるとラムダがよりいっそう楽しそうな気がするのですが、どうでせうか。Lombokみたいな感じで実現できないだろうか?
 絶対、どっかの誰かが提案してるんじゃないかな?

jshellを自分のプログラムに組み込みたかった

はい。タイトルの通りです。しょーもないことしてました。


jshell

JDK9から付属することになっているちまたで話題のJavaのREPL。
私は全く興味がなかったんですけど、ふと思ったんですよ。これ自分のプログラムに組み込めないかな?


GUI系のプログラムは毎回実行して動作確認して終了して直して実行して………が面倒くさいんですよ。
JavaScriptとかGroovyとかを実行できるような簡易GUIを搭載したりしてそこでチマチマ弄れるようにしたりしてるんですけど、これをjshellに置き換えられないかな?と調べてみました。


自分のプログラム内部でJShellを作成して実行する

JShellは以下のような感じで作成できるようです。

try(JShell shell = jdk.jshell.JShell.create()){
}

 というわけで、以下のようなコードを試してみました。

package test;

import java.util.List;
import jdk.jshell.*;

public class TestApplication {
    
    public static String PRINT = "------Not Change-------";
    
    public String method(){//JShellでこのメソッドを呼ぶ
        System.out.println(PRINT);
        return "Test Application";
    }
    
    public static void main(String[] args){
        PRINT = "Print this text?";//ここでPRINTを入れ替えている
        try(JShell shell = JShell.builder()
                //err,outで出力先を変えられる
                .err(System.out)
                .out(System.err)
                .build()){
            shell.addToClasspath(System.getProperty("java.class.path"));
            List<SnippetEvent> evs =  shell.eval("new test.TestApplication().method();"); 

            for(SnippetEvent e:evs){
                if (e.causeSnippet() == null) {
                    switch (e.status()) {
                        case VALID:
                            if(e.value()!=null)
                                System.out.println("eval result = "+e.value());
                            else
                                System.out.println("Success.(no result)");
                            break;
                    }
                }
            }
        }
    }
}

 もし、私の望み通りの動作をするならば、「Print this text?」という文字が出力された後に、「eval result = Test Application」となるはずです。

結果

f:id:nodamushi:20161130221852p:plain

 ………残念でした。------Not Change-------とプリントされてしまいました。
 つまり、JShellエンジンではmain関数は実行されていないことになっているのです。(main関数の一番最初にPRINT変数を入れ替えている)


 どうもJShellは実行時にもう一個別のJVMを立ち上げて、そちらをリモートで操作するような処理をしているようです。
 上記のプログラムのmethod()でsleepするように書き換えてから実行すると、2個Javaのプロセスが増えるのを確認できます。

f:id:nodamushi:20161130221853p:plain


それでもjshellを組み込みたい

>jshell -cp 必要なクラスパス

jshell>test.TestApplication.main(new String[0]);

ん~~………

Java9のVarHandleを使ってみた

 注意:この記事はJava9が公開される前、アーリーアクセス版時代の話です。今?試してないけど、変わって無さそうでしたよ。
 AtomicIntegerを使う必要が出てきた場面で、ふとVarHandleのことを思い出し、ちょっと試してみました。
 試したコードは最後に載せておきますが、やってみたことはこんな感じ。

  • staticなint型変数のVarHandleの取得
  • setとsetVolatileの実行
  • int,volatile int,int(synchronized),AtomicIntegerで並列カウントし、結果の出力と簡易的な時間の計測



VarHandleの取得

 まずこの時点でつまった。JavaDocに書いておいてよ、取得方法。※Java9が公開された現在、JavaDocに詳しく書いてあります。
 たぶん、こんな感じ。まず、Hogeにstaticなaと、フィールドメンバのbがあるとする。

public class Hoge{
  static int a;
  int b;
  int[] arr;
}

 これの取得はたぶん、こんな感じ。(後述するテストコードではlookup().in(Test.class)というのをキャッシュしてから実行してる)

java.lang.invoke.MethodHandles.lookup()
                .findStaticVarHandle(Hoge.class,"a",int.class);
java.lang.invoke.MethodHandles.lookup()
                .findVarHandle(Hoge.class,"b",int.class);
//配列の「要素」を操作したい場合は以下のメソッドでVarHandleを取得する
java.lang.invoke.MethodHandles.arrayElementVarHandle(int[].class);




代入

 VarHandleのJavaDocを眺めていると、値を代入する単純なメソッドはsetとsetOpaque,setReleaseとsetVolatileがあるっぽい。

  • setは普通の代入の代わりっぽい。
  • setOpaqueはプログラムオーダーは保証されるけど、スレッド間のメモリの整合性を考慮しない普通の代入?
  • setReleaseはリオーダーがされないことが保証されるっぽい。setReleaseを使えばJavaでダブルチェックロッキングパターンが実装可能な感じかな?
  • setVolatileがvolatileが付与されているかのごとく代入するっぽい。setReleaseとの差はよく分からん。

 まぁ、よくわからんけど、volatile修飾子がついていてもsetで代入できるし、volatile修飾子がついていなくてもsetVolatileが実行できるっぽい。

        //------ set COUNTER ----------------------
        System.out.println("---------Method set------");
        COUNTER_HANDLE.set(10); // volatileではない変数のハンドル
        V_COUNTER_HANDLE.set(10);//volatileな変数のハンドル。setVolatileじゃなくても代入可能

        System.out.println("---------Method setVolatile------");
        COUNTER_HANDLE.setVolatile(0);//volatileではない変数のハンドル。setじゃなくても代入可能
        V_COUNTER_HANDLE.setVolatile(0);////volatileな変数のハンドル。

 なお、staticな変数でない場合は、以下のようにする

handle.set(obj,0)

 配列の要素を操作したい場合は、以下のようにする

handle.set(arr,index,value)




カウンターの測定

 以下の4つの条件でカウンターを動かし、実行時間の測定と、最終結果が正しいかどうかを確認する。

  • volatileな変数をVarHandleでインクリメント
  • volatileな変数をVarHandleでインクリメント
  • 非volatileな変数をsynchronizedした関数でインクリメント
  • AtomicIntegerでインクリメント

 ちなみに、2016/11/28日現在のJDK9のソースコードではAtomicIntegerはまだUnsafeを使っているようです。

 まずは、シングルスレッドで20億回インクリメントして測定したもの。(※後述のソースコードには存在していない

内容 最終結果 計測時間
volatileな変数をVarHandleでインクリメント 2000000000 約10秒
volatileな変数をVarHandleでインクリメント 2000000000 約10秒
非volatileな変数をsynchronizedした関数でインクリメント 2000000000 約40秒
AtomicIntegerでインクリメント 2000000000 約10秒

 
結果からシングルスレッドではVarHandleとAtomicIntegerの速度は同等であることが言える。


 次に200万回インクリメントする関数を、1000スレッド起動して実行し、最終結果と実行時間を計測した。実行時間の計測には本当ならJVMが安定するまでの時間等を考慮するべきだけど、結構時間かかる処理だから誤差でしょってことでその辺は考慮していない。

内容 最終結果 計測時間
volatileな変数をVarHandleでインクリメント 2000000000 約40秒
volatileな変数をVarHandleでインクリメント 2000000000 約40秒
非volatileな変数をsynchronizedした関数でインクリメント 2000000000 約38秒
AtomicIntegerでインクリメント 2000000000 約40秒

 おんや?volatileでなくても結果が正しく、また、何度実行してもsynchronizedが他のよりも2,3秒早いという結果になりました。
 前者についてはJavaDocを読んでみると、VarHandle.getAndAdd()が内部でVarHandle.setVolatile()を使っているようで、volatileがついていなくてもvolatileがついているのと同等に扱われたためかと思います。
 後者については、AtomicIntegerとかVarHandleはCPUが常に100%になるのにたいして、synchronizedするとスレッドが待機状態になるから、あまりCPUリソースを使わないのが原因かと思います。
 
 これではちょっと結果としてどうかという気がしたので、条件を変更して250000000回インクリメントする関数を8スレッド動かしてみました。この位のスレッド数なら、通常でもあり得るでしょう。

内容 最終結果 計測時間
volatileな変数をVarHandleでインクリメント 2000000000 約38秒
volatileな変数をVarHandleでインクリメント 2000000000 約38秒
非volatileな変数をsynchronizedした関数でインクリメント 2000000000 約64秒
AtomicIntegerでインクリメント 2000000000 約38秒

 おんやぁ?私の予想に反して、synchronizedがえっらい遅くなっただけですね。(VarHandleとかがもうちょっと早くなるかと思ってた)
 ま、まぁ、synchronizedが遅いという結果が得られたので良しとしましょう。

 上記の結果をまとめると以下のようになる。

  • AtomicInteger(Unsafeを使用)とVarHandleは速度が同等
  • volatileであってもなくても、VarHandle.getAndAddが中でsetVolatileを使っているためか結果は同じ
  • volatileであってもなくても、VarHandleを使った場合実行時間は同じ



使ってみた所感


 ぶっちゃけ使いにくい。使うの面倒くさいわー。AtomicIntegerで良いと思うわー。


 かつて、以下↓のような構文が使えるようになるとか何とか聞いた気がしたんだけど、早くプリーズ。

 field.volatile.incrementAndGet();



テストコード

 

import java.lang.invoke.*;
import java.util.concurrent.atomic.*;

public class Test{

    private int a = 0;
    private int[] arr = new int[10];
    private final VarHandle aHandle,arrHandle;

    public Test(){
        VarHandle ahandle=null,arrhandle=null;
        try{
            ahandle=MethodHandles.lookup().findVarHandle(Test.class,"a",int.class);
            arrhandle=MethodHandles.arrayElementVarHandle(int[].class);
        }catch(Throwable t){
            t.printStackTrace();
        }
        this.arrHandle =arrhandle;
        this.aHandle =ahandle;
        aHandle.set(this,100);
        System.out.println("a = "+a);
        arrHandle.set(arr,5,100);
        System.out.println("arr[5] = "+arr[5]);
    }



    private static int COUNTER=1234;
    private static final VarHandle COUNTER_HANDLE;//COUNTERのハンドル

    private static volatile int V_COUNTER=5678;//※ volatileを付加
    private static final VarHandle V_COUNTER_HANDLE;//V_COUNTERのハンドル

    private static int S_COUNTER = 0; // synchronizedな関数でカウントする為の変数

    private static final AtomicInteger ACOUNTER=new AtomicInteger(0);



    //----  VarHandleの初期化。たぶんこんな感じ---------
    static{
        VarHandle handle = null,vhandle=null;
        try{
            MethodHandles.Lookup lookup = MethodHandles.lookup().in(Test.class);
            handle = lookup.findStaticVarHandle(Test.class,"COUNTER",int.class);
            vhandle = lookup.findStaticVarHandle(Test.class,"V_COUNTER",int.class);
        }catch(Throwable t){
            t.printStackTrace();
        }
        COUNTER_HANDLE = handle;
        V_COUNTER_HANDLE = vhandle;
    }
    //-----------------------------------

            


    public static void count(){
        COUNTER_HANDLE.getAndAdd(1);
    }

    public static void vcount(){
        V_COUNTER_HANDLE.getAndAdd(1);
    }

    public static synchronized void scount(){
        S_COUNTER++;
    }



    public static final int LOOP = 2000000000/8;
    public static final int THREADS = 8;
    public static void run1(){
        for(int i=0;i<LOOP;i++){
            count();
        }
    }
    public static void run2(){
        for(int i=0;i<LOOP;i++){
            vcount();
        }
    }
    public static void run3(){
        for(int i=0;i<LOOP;i++){
            scount();
        }
    }
    public static void run4(){
        for(int i=0;i<LOOP;i++){
            ACOUNTER.incrementAndGet();
        }
    }


    public static void main(String[] args){
        new Test();


        showCounter();
        showVCounter();
        
        //------ set COUNTER ----------------------
        System.out.println("---------Method set------");
        COUNTER_HANDLE.set(10);
        V_COUNTER_HANDLE.set(10);//setVolatileじゃなくても代入可能
        showCounter();
        showVCounter();
        System.out.println("---------Method setVolatile------");
        COUNTER_HANDLE.setVolatile(0);//setじゃなくても代入可能
        V_COUNTER_HANDLE.setVolatile(0);
        showCounter();
        showVCounter();


        System.out.println("---------Run1 start------");
        long t = System.currentTimeMillis();
        Thread[] ths  = new Thread[THREADS];
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run1);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        showCounter();
        System.out.println("Time = " + t/1000d);


        System.out.println("---------Run2 start------");
        t = System.currentTimeMillis();
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run2);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        showVCounter();
        System.out.println("Time = " + t/1000d);



        System.out.println("---------Run3 start------");
        t = System.currentTimeMillis();
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run3);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        System.out.println("S_COUNTER="+S_COUNTER);
        System.out.println("Time = " + t/1000d);

        System.out.println("---------Run4 start------");
        t = System.currentTimeMillis();
        for(int i=0;i<THREADS;i++){
            ths[i] = new Thread(Test::run4);
            ths[i].start();
        }
        for(int i=0;i<THREADS;i++){
            try{
                ths[i].join();
            }catch(InterruptedException e){}
        }
        t = System.currentTimeMillis() - t;
        System.out.println("ACOUNTER="+ACOUNTER.get());
        System.out.println("Time = " + t/1000d);
        
    }


    public static void showCounter(){
        System.out.print("COUNTER =");
        System.out.println(COUNTER_HANDLE.get());
    }
    public static void showVCounter(){
        System.out.print("V_COUNTER =");
        System.out.println(V_COUNTER_HANDLE.get());
    }

}

組み込み屋初心者さんのためのコーディング技術(C言語)

 私が最近生存している業界は、アナログ最後の砦と言われているほどIT化の遅い世界ですが、ここ数年でジワリジワリとソフトの力が増してきている、という業界です。
 あ、なお、お前のブログ読んでるとまるで働いているみたいだな!とか言われましたが、私はクソニートですので誤解なきよう。お仕事くらさい。

 で、そんなもんですから、今までアナログ屋だった人が突然ディジタル屋に突っ込まれたりするわけで、コーディング技術何それおいしいの?状態だっりします。
 そんな方々が書くCコードを見ると、ひたすらコピペの連続のような力業プログラミングだったりするわけです。


 そんな方々に送るちょっとしたアドバイス記事です。別段目新しかったり、難しい内容はありません。

 なお、この記事におけるコーディング技術は主に「保守管理」に関わる様な技術を指しており、アルゴリズムの話や、超絶技法の様な話は含んでおりません。




コーディング技術は何故必要なのか?

 そもそも、なぜコーディング技術が組み込み屋に必要なのでしょうか?
 大規模組み込みならまだしも、ちっこいマイコン相手にしているだけだと、コーディング技術とかただの理想論にしか聞こえないかも知れません。そういった方々にとって大事なことは「動くこと」だけになっています。

 むろん、動くことは何よりも重要です。どれほど素晴らしく、美しいコードであっても、実機で動かなければウンコほどの価値もありません。
 しかし、「簡単に書く」、「簡潔に書く」、「わかりやすく書く」という観点が抜けてしまうと、何故ソフトウェア化するのか?の意義の一つが減ってしまいます。

 ソフトウェア化する意義とは何か?
 今、我々の業界で進んでいるソフトウェア化は、基本的にはハードをソフトで置き換えているだけです。となると、現状あるソフトウェア化のメリットは、「部品点数削減」これだけです。
 ソフト化による製品開発期間の短縮?「今」のあなた方はそれが出来るほど、今のあなた方にソフトの技術がありますか?
 ディジタル化することによる、アナログで出来ない高性能化?それが既に達成されている業界では最も重要なことだと思いますが、我々の業界ではまだ理想論であり、現状は物にするほど理論は完成されていません。それは数年という期間を経て実現することです。もし、それが本当にソフトウェア化のキーワードであるのであれば、短い開発のサイクルで製品化するようなプロジェクトではなく、数年という長い期間を経て市場に出すべきでしょう。


 それでもなお、今、慣れないディジタル技術を用い、短い開発期間で製品開発を強いられている意義は、部品点数削減のためです。
 たしかに、ソフト化することで、コンデンサの数が減れば、物理的な耐久性の向上、部品コストの削減が図れます。しかし、ここで一つ考えなければならないのは、製品の部品は減っていないということです。なぜなら、物理的部品が減った代わりに、プログラムコードという部品が増えたからです。
 コードの価値というのは低く見積もられがちですが、コードは部品であり、使わないコードや重複するコードがあれば、それは在庫です。
 このコードという部品をどう管理するのか?これは当然考えなくてはならない事です。

 果たして、製品コードに問題があると判明したとき、貴方はそのコードを後で見返して、すぐにどこに問題があるかわかるでしょうか?貴方が修正するならまだしも、貴方が忙しくて他人に頼む場合、そもそも貴方がそこにいなくなっていた場合、他人はすぐにどこに問題があるか分かるでしょうか?
 問題がなかったとしても、別な製品でそのプログラムを、使い回したい場合、それがすぐできますか?

 これらが出来る場合、当然出来ない場合に比べて製品のコストが下がることになります。逆にこれが出来ない場合、もしかしたらソフト的なコストで、むしろ製品コストが増加することすら、あるかも知れません。


 そこで必要になるのが、如何に簡潔に、綺麗に、わかりやすく書くかというコーディング技術です。


コーディング技術なんて捨てて良い


 コーディング技術?残念だが、そんなものは糞食らえだ。今すぐ捨てて貰って構わない。


 さっきと言っていることが、真逆で申し訳ないのだが、やはりこれは最初に書いておく必要があると思う。コーディング技術に頼ってはならない。


 組み込みの世界はどうしても、使えるリソースが少ないです。リソースというのは、メモリであったり、実行ステップ数(実行時間)だったり、得られる情報などを指します。
 これらの条件と、コーディング技術は相反する場合があります。そういった場合、迷うことなくコーディング技術を捨てて、泥臭くて汚い力業の世界に持ち込んでください
 如何に綺麗に書くかを悩むだけ時間の無駄です。

 ステップ数が足りない?そうか、そこの関数呼び出し止めて展開しようか。そのループを展開しようか。ポインタ操作の最適化しようか。そこの計算アセンブリにしようか。
 メモリが足りない?なら、この変数とあの変数は同時に使わないから、同じ変数にしよう。
 組み込みでは、どうしてもこういう事態が発生します。


 わかりやすく書くコーディング技術は大切なことですが、捨てて構わないものです。
 大切なのはバランス感覚です。


早すぎる最適化はしない。コーディング技術を大切に

 関数呼び出しをやめて展開する。ループを展開する。ポインタの最適化。メモリ配置の最適化。
 こういったものを行いたいという欲求は、早すぎる最適化を呼んでしまいます。こういった手段は、最後の奥の手です。
 最適化をしなくても動くのであれば、最適化などする必要がありません。汎用性を失うだけです。

 例えば、設定する変数A、B、Cがあり、A,Cが隣り合うアドレスだったとしましょう。
 このとき、「A=1;B=2;C=3;」よりも、「A=1;C=3;B=2;」の方が上手い具合に最適化される可能性が高いですね。
 コンパイラが信用できないなら、こう書けば完璧です。

char *p = &A;
*p = 1;
*(p+1)=3;
B=2;

 さて、このコード、使用しているマイコンの次バージョンを使用するときにも使えるのでしょうか?
 ソースコードレベルでの後方互換があったとしても、アドレスまでは完全に同じとは限りません。そうなると、このコードは再利用は出来ない在庫コードとなります。

 省メモリ、小ステップは確かに大切なことですが、クリティカルな問題にならない限りは、最初は放っておいても良いのです。
 大切なのはバランス感覚です。



数値はdefineで名前を付けましょう

 マジックナンバーと言う言葉をご存じでしょうか?マジックナンバーとは、処理を知っている人だけが理解できる、プログラム中に現れる他人から見たら謎の数値のことです。
 組み込みではこのマジックナンバーが大量に現れます。

MODULE0 = 0x6c;
MODULE0OV = 1234;
MODULE0EN = 0x71;

 さて、この0x6c,1234,0x71って何でしょうね。意味が分からないですね。
 これを理解するには、マイコンの仕様書を片手に各ビットが何なのかを調べなくては、読むことが出来ません。吐き気がします

 それを回避するためにこんなコメントが書かれてたりします。

MODULE0 = 0x6c; // MODULE0の結果転送先を~~に切り替え。Event出力あり。クロックはクロック1
MODULE0OV = 1234; // オフセット値
MODULE0EN = 0x71; // MODULE0をイネーブル。割り込みOn

 確かに少し読めるようになりました。しかし、根本的に何も解決していません。
 例えば、MODULE0の結果転送先を変えたいとき、0x6cはどう数値を弄れば良いのでしょうか?

 なに?仕様書をみろ?
 バカじゃないの?マイコンの仕様書とか分厚いのに、ちょっと数値弄るためだけに何度も見直すなんて。


 こういう場合は、数値の意味を分解します。
 例えば、クロックを決定しているのが、上位2bit(7,6bit)、Event設定がその次のビット(5bit)、4bitは例えばモード切り替えで、下位4bitが転送先設定としましょう。
 すると、次のように意味単位で数値を分離できます。

MODULE0 = 0x40 | 0x20 | 0x00 | 0x0c;//クロックは1 | Eventを送る | モードは0 | 転送先は~~

 別にこの場合は+(加算)でも構わないのですが、|(ビットor)を良く使います。どのみち、組み込み屋はビット演算からは逃げられないので、諦めてビットorに慣れてください。

 さらに、これらの数値にdefineで名前を与えると次のようになります。

#define MODULE_CLOCK0 0x00
#define MODULE_CLOCK1 0x40
#define MODULE_CLOCK2 0x80
#define MODULE_CLOCK3 0xc0

#define MODULE_SEND_EVENT 0x20
#define MODULE_UNSEND_EVENT 0x00

#define MODULE_MODE0 0
#define MODULE_MODE1 0x10

#define MODULE_TRANSFER_C 0x0c

MODULE0 = MODULE_CLOCK1 | MODULE_SEND_EVENT | MODULE_MODE0 | MODULE_TRANSER_C;

 随分わかりやすくなりました。もし、モジュールからイベントを送りたくなくなった場合は以下のように変えれば良いだけです。

MODULE0 = MODULE_CLOCK1 | MODULE_UNSEND_EVENT | MODULE_MODE0 | MODULE_TRANSER_C;


 ちなみに、このように分離することで、無駄な処理が入るから使えないと思うかも知れませんが、0x6cと各コードとこのコードは全くコンパイル結果は同じです。
 定数式は最適化オプションを切っていてもコンパイラが勝手に計算してくれます。





 え、マイコン対応コンパイラがガチで計算してくれないドマイナー糞コンパイラだって………?
 どれほど安いのか知りませんが、そのマイコン使うの辞める事を提案するところから始めませんか?



コメントを書く様にする

 コードを書いた時点ではわかっていることでも、3日もすればすぐに分からなくなるものです。
 明日の自分は他人です。何も知らない人に優しく説明するように、明日の自分のためにコメントと言う名のドキュメントを残しましょう。

 特に、グローバル変数や関数などには、使い方などを残しておくのが吉です。
 呼び出し条件は?この引数の意味は?引数に渡して良い値は?渡してはならない数値は?などはキッチリ残しておかないと、絶対後悔します。
 Javadocの様に後でドキュメント化する場合は、Doxygenを使うと良いでしょう。

/*!
 * 1分の内にHOGE割り込みが起こった回数を記録するためのカウンタ
 * HOGE割り込み関数とタイマの関数以外から触ることを禁ずる
 */
volatile unsigned int hoge_interrupt_counter;

/**
 * @fn
 * モジュールの再初期化をする。
 * モジュールが既に動作しているときのみ呼び出すこと。
 * それ以外で呼び出した場合の動作は保証しない。
 *
 *
 * @param mode 0,1,3だけ。2,4は禁ずる。この責任は呼び出し側にあるとする。
 * @param offset_value 飽和処理はこの関数が責任を持つ。
 */
void reinitModule(char mode,char offset_value)
{


 なるべく呼び出し元の責任が減るように関数内で判断するのが良いのですが、組み込みだとそうもいかないこともあると思いますので、責任関連は絶対残してください。




コメントを書かない様にする

 ソースコード中に現れるコメントは大別すると以下の3つしかありません。

  1. 有害なコメント
  2. いずれ有害になるコメント
  3. ソースコードとしては意味のないコメント


 従って、書いても良いコメントは無害な「ソースコードとしては意味のないコメント」だけです。
 例えば、以下のようなコメントは書いても良いです。

/*
 *  author:nodamushi
 *  Copyright:ニート有限会社
 */


 //-------------------------------------------------------------


/* ごめんなさい。私を探さないでください。本当にごめんなさい。 */

 その他のコメントは有害なので、極力書かないことをお勧めします。

 有害とはどういうことか?それは、コメントに書いてあることと、実際のコードが異なっている状態を指します。


 先ほどの例を再度挙げてみましょう。

MODULE0 = 0x6c; // MODULE0の結果転送先を~~に切り替え。Event出力あり。クロックはクロック1
MODULE0OV = 1234; // オフセット値
MODULE0EN = 0x71; // MODULE0をイネーブル。割り込みOn

 この時点ではまだ有害ではありません。

 さて、仕様が変わり、Event出力をoffにしました。

MODULE0 = 0x4c; // MODULE0の結果転送先を~~に切り替え。Event出力あり。クロックはクロック1
MODULE0OV = 1234; // オフセット値
MODULE0EN = 0x71; // MODULE0をイネーブル。割り込みOn

 この瞬間、コメントが有害化しました。Event出力をしなくなったはずなのに、コメントは修正されず、「Event出力あり」となっています。しかもこれ、コンパイルエラーにはならないので、結構直すの忘れるんですよね。


 生ものは冷蔵庫に放っておくのではなく、すぐに食べるなりして残さない、もしくは完全に凍結してしまうかのどちらか以外扱い方はありません。
 コメントも同じです。コメントは生ものです。いずれ腐ります。コメントは残さない、もしくはソースコードを二度と弄らない凍結状態とすることが鉄則です。

 コメントに頼らなくても、わかりやすく、「読めるコード」を書くことを心がけましょう。
 変数名がa,b,c,dなんかだったりして、コメントがなくて読めるでしょうか?関数名がhogeだったりして、何の関数なのか分かるでしょうか?
 何百行もひたすら、一つの関数のコードだけ書いてあって、果たして全体が把握できるでしょうか?

 コメントに頼らず、読みやすいコードを書こうとすると、自然と構造がわかりやすいプログラムになっていきます。


 読めるコード、「リーダブルコード」というキーワードを頭の片隅に置いておいてください。

 マジックナンバーを消すのもリーダブルコードの一つですね。



 カレーは飲み物、コードは読み物です。



意味の区切りでなるべく関数に分ける 繰り返し出てくる処理は関数にまとめる


 組み込みではないのですが、サブルーチンも使わずひたすら同じ事をコピペによって繰り返すコードを見たことがあります。
 なんでサブルーチンにしないのか?と聞いてみたら、サブルーチンにするより、コピペの方が簡単だし、安心だからとの返答をいただいた。残念ですが、私には何を言っているのか理解できませんでした。


 まず、コピペは危険です。コピペコードの修正し忘れによるバグの発生はよくありますし、そもそもコピペコードがバグってたときは悲惨で、あちこちに散乱したコピペコードを全て修正する必要があります。どう考えても一カ所にまとめて管理した方が安全です。バグってた場合はその一カ所だけ直せば良いのですから。

 次に、コピペでない場合でも、ひたすら処理が何百行と続いているコードよりも、関数で適切に区切りがあるコードの方が読みやすいです。
 でもこれに対しては、あちこちに処理が散らばるよりも、一カ所の関数だけで書いてある方が読みやすいと言われたことがあります。しかし、関数で区切ることは、文章で言えば、漢字や句読点、章立てと同じです。
 なんぜんぺーじもあるほんがかんじもくとうてんもつかわずましてやもくじもないばあいをかんがえてみてくださいあなたはそのほんをさいごまでよみとおすことができますかわたしはこのぶんしょうですらたぶんあしたにはよめません

 また、長すぎるコードはテストが大変です。問題がどこにあるのかの切り分けが難しいのです。(組み込みでのテストの作り方は私は詳しくないので突っ込みませんが………)
 関数は10~100行ぐらいにまとめると良いと思います。

 あとは、ファイルスコープなどを駆使して、グローバル変数が見える範囲とかも最小限にするなど………まぁ、これはいいや………。



マクロを使って、同じ内容を繰り返し書かない様にする


 マイコンでは、各モジュールの設定手順は、変数名レベルでほとんど同じって事がよくあります。

//モジュールAの0の設定
A0PROP =11;
A0PROP2=22;
A0PROP3=33;

//モジュールAの1の設定
A1PROP =111;
A1PROP2=222;
A1PROP3=333;

//モジュールBの0の設定
B0PROP =1111;
B0PROP2=2222;
B0PROP3=3333;


 こういう場合はマクロでさっくりまとめられます。

/*
 各モジュールのPROPを設定するマクロ

  name:モジュール名
  n:モジュール番号
  p1:PROPの値
  p2:PROP2の値
  pe:PROP3の値
*/
#define SetModuleProps(name,n,p1,p2,p3) name##n##PROP = p1;\
                                        name##n##PROP2= p2;\
                                        name##n##PROP3= p3;

//モジュールAの0の設定
SetModuleProps(A,0,11,22,33);

//モジュールAの1の設定
SetModuleProps(A,1,111,222,333);

//モジュールBの0の設定
SetModuleProps(B,0,1111,2222,3333);


 マクロは関数ではなく、ソースコード文字列の置換なので、これら二つのコードは全く同じコードです。



参考:
  プリプロセッサ
  #と##演算子



マクロは使わない

 マクロは定数マクロ以外使わないでください。(定数マクロは「#define A 10」の様に定数に名前を付けるマクロです。)



 次のように、ほとんど書いている内容が同じだからと言うことで、マクロを使ってまとめるアホがいますが、お勧めは出来ません。
 (マクロでまとめることはお勧めしませんが、関数でまとめることはお勧めします)

/*
 各モジュールのPROPを設定するマクロ

  name:モジュール名
  n:モジュール番号
  p1:PROPの値
  p2:PROP2の値
  pe:PROP3の値
*/
#define SetModuleProps(name,n,p1,p2,p3) name##n##PROP = p1;\
                                        name##n##PROP2= p2;\
                                        name##n##PROP3= p3;

//モジュールAの0の設定
SetModuleProps(A,0,11,22,33);

//モジュールAの1の設定
SetModuleProps(A,1,111,222,333);

//モジュールBの0の設定
SetModuleProps(B,0,1111,2222,3333);

 このように書くぐらいなら、下手なことをせず、以下のように書きましょう。

//モジュールAの0の設定
A0PROP =11;
A0PROP2=22;
A0PROP3=33;

//モジュールAの1の設定
A1PROP =111;
A1PROP2=222;
A1PROP3=333;

//モジュールBの0の設定
B0PROP =1111;
B0PROP2=2222;
B0PROP3=3333;


 なぜなら、これら二つのコードは等価ではありません。………ん?同じじゃないかって?

 これね、ブレークポイントが非常に扱いにくいんですよ。
 マクロを展開すると、以下のようになるから、A0PROP2の所にブレークをおきたくても、エディタ上からは設定できなくて、直にブレークするアドレス探して設定するなんて手間がかかる。

//モジュールAの0の設定
A0PROP =11;A0PROP2=22;A0PROP3=33;

//モジュールAの1の設定
A1PROP =111;A1PROP2=222;A1PROP3=333;

//モジュールBの0の設定
B0PROP =1111;B0PROP2=2222;B0PROP3=3333;

 コンパイラやデバッガが頑張ってくれて、A0PROP2,A2PROP2,B0PROP2の場所にブレークポイントを設置できる場合があったとしても残念なことになる。マイコンなんてしょぼいデバイスは大概ブレークポイントも一、二個しかおけないから、今回のように3箇所に展開されてしまうと、どうもならない。


 その他の理由としてはマクロは難しいし、色々面倒くさいという理由もあります。C++ではプリプロセッサ界は魔界と呼ばれております。
 触らぬ神にたたりなし。不必要に関わらないようにしましょう。




ローカル変数は書き換えない、使い回さない

 
 状態が変化するものを完璧に把握し、完璧にプログラムを組み上げるのは中々難しいものです。
 「AならばB」という明快なプログラムは、ミスしにくいです。しかし、「AならばBが基本だけど、状態によってはCで、まれにDとかEにする」ってなると、一気にバグが入り込みやすくなります。
 なので、本当は「状態に依存しないプログラムを書くことを心がける」と書きたかったのです。なのですが……、組み込みで状態に依存しないプログラムなんて不可能です。基本的にほとんど全ての処理が状態に依存しまくりですよね。


 しかし、状態の変化は諦めるとしても、ローカル変数を書き換えない、使い回さないという意識は出来るとおもいます。
 何カ所にも渡って変数が書き換えられると、その都度、変数の意味を再解釈しなくてはなりません。どこで変化し、どのような意味を持つのかを、全て把握するのは大変です。さらに、もうこの変数はここから先で使わないから、再利用しようと変数を初期化したものの、後で前の結果が必要になることもあります。しかし、そのコードを書く頃には、変数を初期化したなんて忘れててバグった………、なんてもうよくありがちですね。

 ローカル変数の再利用は、変数を定義するのが面倒だったのかも知れません。しかし、後々、もっと大きな面倒を運んできてしまいます。面倒だったのではなく、メモリの使用量の最適化だったのかも知れませんが、それは早すぎる最適化です。
 ローカル変数の値は一度決定したら、書き換えないことは徹底しましょう。

int temp = 1;
………
temp += hoge(1);
………
moge(temp);//実はここでtempの値が変わる
………
//しばらくして
temp = 0;//tempを再利用

....

if(temp == a){//実はここのtempはtemp=0されてることを忘れてる
...
}

 

 




まとめ

  1. 数値には名前を付けましょう。ビット単位で区切るとなお良し
  2. コメントをちゃんと書きましょう。どうせ明日には分からなくなります
  3. コメントを書いてはいけません。どうせ明日には腐ります
  4. 関数で小さく意味の単位に分けましょう
  5. マクロを使って処理をまとめましょう
  6. マクロは地雷踏む可能性高いので使うのは止めましょう
  7. 状態が変わらないほどバグりにくいです。せめてローカル変数だけでも変化しないようにしましょう
  8. 最適化は重要だけど、最後の手段。早すぎる最適化は駄目です


 綺麗なコードを書いて苦しい組み込みライフを楽しんでください。
 (´・д・`)ノシ

EclipseCDTにC方言を追加するプラグインを作ろう その2

 さて、前回BNFで拡張構文の規則を作りました。今回からは、その規則に対するJavaの処理を作っていきます。

構文解析の流れ

 Javaの処理を作る前に、CDTがどのような手順で構文を解析しているかについて、ザックリと解説します。

  1. IScannerインターフェースを実装した字句解析器(CPreprocessor)でテキストをITokenに分解
  2. 字句解析器が生成したITokenを、構文解析器で使えるように変換(今回の記事の主内容)
  3. 変換したITokenを構文解析器に渡す
  4. 構文解析器の規則に従ってITokenからIASTNodeを作成する

 

 IScannerの実装であるCPreprocessorは、名前からわかるようにプリプロセッサの処理もしてくれます。このプリプロセッサの処理があるためか、字句解析器を自作することはできないようです。

 さて、CPreprocessorが作ってくれたITokenを構文解析したいのですが、実はこのままでは構文解析器は動きません。例えば、SDCCでは終端記号として「__sfr」などを定義しました。これは終端記号であって、終端文字列ではありません。記号には、IToken.getType()で得られる整数値を用います。同じ終端文字列であっても、CPreprocessorが作るITokenのtypeと、我々が作った構文解析器が受け付けるITokenのtypeが異なっているため、我々が実装した構文解析器は動きません。この差を吸収するためのコンバータが必要になります。

 最終ステップで、構文解析器は定義されている規則にマッチすると、登録されているJavaの処理を起動してくれます。ここでマッチした規則や、ITokenの内容からIASTNodeを作成します。



 だいたいこんな感じです。ここで作られたIASTNodeから関数名のリストを作ったり、色々しているみたいですが、その辺は詳しく知りません。また、IScannerに渡すテキストも、実は全部のテキストじゃなくて、ブロック単位で管理されていたりと、色々処理が入っています。


ITokenを変換するIDOMTokenMapの実装


 先にも説明したように、CPreprocessorのIToken.getType()の値を、我々が扱える数値に変換する必要があります。この変換機能を提供するインターフェースがIDOMTokenMapです。IDOMTokenMapのインスタンスは、前回作ったBNFをコンパイルすることで生成されるParserクラスのコンストラクタに渡す必要があります。

 我々が扱える数値は、grammarファイルをコンパイル(grammerディレクトリでantを実行)すると生成される~~Parsersymインターフェースに、「TK_終端記号名」という名前で定義されています。(参考:SDCCParsersym.java)ちなみに、この~~Parsersymインターフェースで文法エラーが発生してる場合は、grammarファイルが既にバグっています。
 一方、CPreprocessorはITokenに定義されている定数「t~~~」を扱います。

Keywordの定義

 特にやらなくても良いですが、IDOMTokenMapを実装する前に、拡張定義したKeywordを定義するenumを作成しておくと、見通しが良くなります。C99で定義されているものまで、定義する必要はありません。

package nodamushi.cdt.parser.sdcc;

//Parsersymの定数をstatic import
import static nodamushi.internal.cdt.parser.sdcc.SDCCParsersym.*;

public enum SDCCKeyword{
  __data(TK___data),
  __near(TK___near),
  __xdata(TK___xdata),
………略……… 

  // わざわざ構文解析器で解析することはやめた追加キーワード
  __asm__(-1) 
  ;

  //対応する数値を保持しておく

  private final int tokenKind;
  private SDCCKeyword(int tokenKind){
    this.tokenKind = tokenKind;
  }

 


 で、ITokenから対応するenumのtokenKindを、素早く検索する為にMapにしておきます。MapはMap<IToken,SDCCKeyword>やMap<String,SDCCKeyword>ではなく、Map<char[],int>で持つ方が良いみたいです。そのためのMapにCharArrayIntMapというのがあったので、これを使いました。UPCの実装ではCharArrayMap<V>が使われていました。

  private static final int EMPTY_NUMBER = Integer.MIN_VALUE;
  private static final CharArrayIntMap tokenMap;

  static{
    final SDCCKeyword[] values = values();
    final int size = values.length;
    tokenMap = new CharArrayIntMap(size, EMPTY_NUMBER);
    sdccKeywords = new String[size];
    for(int i=0;i<size;i++){
      final SDCCKeyword v = values[i];
      final String name = v.name();
      if(v.tokenKind != -1) // 構文解析器と無関係なものは無視
        tokenMap.put(name.toCharArray(), v.tokenKind);
    }
  }
  
  // OptionalIntを使ってるのはただの趣味
  public static OptionalInt getTokenKind(char[] image){
    if(image == null){
      return OptionalInt.empty();
    }else{
      int v = tokenMap.get(image);
      return v == EMPTY_NUMBER? OptionalInt.empty():OptionalInt.of(v);
    }
  }

 

IDOMTokenMapの実装

 実はIDOMTokenMapの実装のほとんどを、先のKeywordでやっちゃっいました。なので、基本的には以下のコードをSDCCやpackage名だけ直せば、そのままで問題ありません。私もUPCからほとんどコピっただけです。

 我々がASTNode変換処理するときに使うITokenは、このIDOMTokenMapのmapKindを通じて得られたtypeに設定されたITokenになります。

package nodamushi.cdt.parser.sdcc;

import org.eclipse.cdt.core.dom.lrparser.IDOMTokenMap;
import org.eclipse.cdt.core.parser.IToken;
import static org.eclipse.cdt.core.parser.IToken.*;
import static nodamushi.internal.cdt.parser.sdcc.SDCCParsersym.*;

public class DOMToSDCCTokenMap implements IDOMTokenMap{
  @Override public int getEOFTokenKind(){
    return TK_EOF_TOKEN;
  }

  @Override public int getEOCTokenKind(){
    return TK_EndOfCompletion;
  }

  @Override public int mapKind(IToken token){
    switch(token.getType()) {
      case tIDENTIFIER : 
        return SDCCKeyword.getTokenKind(token.getCharImage()).orElse(TK_identifier);

      //以下はC99の変換(誰が作っても固定だと思う)
      case tINTEGER      : return TK_integer;
      case tCOLON        : return TK_Colon;
      case tSEMI         : return TK_SemiColon;
      case tCOMMA        : return TK_Comma;
      case tQUESTION     : return TK_Question;
      case tLPAREN       : return TK_LeftParen;
      case tRPAREN       : return TK_RightParen;
      case tLBRACKET     : return TK_LeftBracket;
      case tRBRACKET     : return TK_RightBracket;
      case tLBRACE       : return TK_LeftBrace;
      case tRBRACE       : return TK_RightBrace;
      case tPLUSASSIGN   : return TK_PlusAssign;
      case tINCR         : return TK_PlusPlus;
      case tPLUS         : return TK_Plus;
      case tMINUSASSIGN  : return TK_MinusAssign;
      case tDECR         : return TK_MinusMinus;
      case tARROW        : return TK_Arrow;
      case tMINUS        : return TK_Minus;
      case tSTARASSIGN   : return TK_StarAssign;
      case tSTAR         : return TK_Star;
      case tMODASSIGN    : return TK_PercentAssign;
      case tMOD          : return TK_Percent;
      case tXORASSIGN    : return TK_CaretAssign;
      case tXOR          : return TK_Caret;
      case tAMPERASSIGN  : return TK_AndAssign;
      case tAND          : return TK_AndAnd;
      case tAMPER        : return TK_And;
      case tBITORASSIGN  : return TK_OrAssign;
      case tOR           : return TK_OrOr;
      case tBITOR        : return TK_Or;
      case tBITCOMPLEMENT: return TK_Tilde;
      case tNOTEQUAL     : return TK_NE;
      case tNOT          : return TK_Bang;
      case tEQUAL        : return TK_EQ;
      case tASSIGN       : return TK_Assign;
      case tUNKNOWN_CHAR : return TK_Invalid;
      case tSHIFTL       : return TK_LeftShift;
      case tLTEQUAL      : return TK_LE;
      case tLT           : return TK_LT;
      case tSHIFTRASSIGN : return TK_RightShiftAssign;
      case tSHIFTR       : return TK_RightShift;
      case tGTEQUAL      : return TK_GE;
      case tGT           : return TK_GT;
      case tSHIFTLASSIGN : return TK_LeftShiftAssign;
      case tELLIPSIS     : return TK_DotDotDot;
      case tDOT          : return TK_Dot;
      case tDIVASSIGN    : return TK_SlashAssign;
      case tDIV          : return TK_Slash;
      case t_auto        : return TK_auto;
      case t_break       : return TK_break;
      case t_case        : return TK_case;
      case t_char        : return TK_char;
      case t_const       : return TK_const;
      case t_continue    : return TK_continue;
      case t_default     : return TK_default;
      case t_do          : return TK_do;
      case t_double      : return TK_double;
      case t_else        : return TK_else;
      case t_enum        : return TK_enum;
      case t_extern      : return TK_extern;
      case t_float       : return TK_float;
      case t_for         : return TK_for;
      case t_goto        : return TK_goto;
      case t_if          : return TK_if;
      case t_inline      : return TK_inline;
      case t_int         : return TK_int;
      case t_long        : return TK_long;
      case t_register    : return TK_register;
      case t_return      : return TK_return;
      case t_short       : return TK_short;
      case t_sizeof      : return TK_sizeof;
      case t_static      : return TK_static;
      case t_signed      : return TK_signed;
      case t_struct      : return TK_struct;
      case t_switch      : return TK_switch;
      case t_typedef     : return TK_typedef;
      case t_union       : return TK_union;
      case t_unsigned    : return TK_unsigned;
      case t_void        : return TK_void;
      case t_volatile    : return TK_volatile;
      case t_while       : return TK_while;
      case tFLOATINGPT   : return TK_floating;
      case tSTRING       : return TK_stringlit;
      case tLSTRING      : return TK_stringlit;
      case tUTF16STRING  : return TK_stringlit;
      case tUTF32STRING  : return TK_stringlit;
      case tCHAR         : return TK_charconst;
      case tLCHAR        : return TK_charconst;
      case tUTF16CHAR    : return TK_charconst;
      case tUTF32CHAR    : return TK_charconst;
      case t__Bool       : return TK__Bool;
      case t__Complex    : return TK__Complex;
      case t__Imaginary  : return TK__Imaginary;
      case t_restrict    : return TK_restrict;
      case tCOMPLETION   : return TK_Completion;
      case tEOC          : return TK_EndOfCompletion;
      case tEND_OF_INPUT : return TK_EOF_TOKEN;

      default:
        assert false : "token not recognized : " + token.getType();
      return TK_Invalid;
    }
  }
}

 じゃ、次回ついにIASTNodeに変換するぜよ。たぶん。

EclipseCDTにC方言を追加するプラグインを作ろう その1

 はい、誰の得になるのかわからない超ニッチシリーズが再び始まりました。

 組み込みでは、GNU CやらVisual C Compilerなんかは使えず、独特なドマイナーコンパイラを使わざるを得ないことがあります。そのドマイナーコンパイラがC標準とかに準拠してくれてたら良いんだけど、コンパイラの拡張構文とか、そもそも準拠してないとかザラにあります。(以降、標準でない拡張や準拠していない構文をまとめてC方言と書きます)

 で、そのC方言を使ってEclipse CDT上で開発すると、こういう風にエラーが出ちゃうんですよね。

f:id:nodamushi:20160725130413p:plain


 今回から解説するのは、こういう風にエディタ上にエラーを出さない様にするために、CDTに方言に対応したパーサーを追加するプラグインをどうやって作るのかって話です。

 この記事ではSDCCに対応するパーサーを作っていきます。
 成果物はこちら。
github.com

概要

 手順は大きく以下の3つになります。

  1. C方言のパーサー(構文解析器)を作る
  2. 構文解析結果をIASTNodeに変換する
  3. パーサーを提供するILanguageを作る


 少しこの手の話に詳しい方なら、「字句解析器」は作らないのか?と思われるかもしれません。ここでは説明を抜きして結論だけ言うと、字句解析器は作りません。CDTが提供するものを使います。

 構文解析器は0から全て自分が作ってもかまいませんが、C言語の仕様を全て理解し、全ての構文を実装するのはなかなか大変です。できることなら方言で拡張された構文だけを実装することで済ましたいものです。というわけで、org.eclipse.cdt.core.dom.lrparserプラグインで提供されるC99パーサーを拡張することでC方言パーサーを新たに作りたいと思います。


環境を整える

 まずはプラグイン開発に必要なものについて。以下の三つが必要です。

  1. Eclipse CDT、C99 LR Parser
  2. Eclipse CDTのソースコード
  3. LALR Parser Generator

 

Eclipse CDT,C99 LR Parser

 Eclipse CDTの開発をするので当然CDTが必要です。すでにCDTを導入済みという人も、オプションであるC99 LR Parserが入ってない可能性があるので、確認をしてください。
f:id:nodamushi:20160728174345p:plain


Eclipse CDTのソースコード

 org.eclipse.cdt.core.dom.lrparserのgrammarをベースにしますので、これをダウンロードしてきます。最初はorg.eclipse.cdt.core.dom.lrparser.sourceプラグインに入ってるかなとか思ったんですけど、肝心のgrammar部分は入っていませんでした。なので、Eclipse CDTのソースコード丸ごと全部ダウンロードしてください。
 私がやったときは、Eclipse CDT/gitのWikiに書いてあるgit://git.eclipse.org:29418/cdt/org.eclipse.cdt.gitからはどうもダウンロードできなかったので、git://git.eclipse.org/gitroot/cdt/org.eclipse.cdt.gitか、GithubのEclipse CDTからcloneしてきてください。

LALR Parser Generator

 次にLALR Parser Generatorをダウンロードしてください。Windowsの人がここからダウンロードすると、2016年現在「lpg-win32_x86_2.0.20」というなぜか拡張子がないものがダウンロードされますので、ファイル名の拡張子として「.exe」をつけてください
 ていうか、このLALR Parser Generatorとか何年も更新されてないし、この元プロジェクトのEclipse IMCってのはもう終わってるみたいだし、大丈夫なのか?って感じですが、少なくともEclipse Neonでは動いたのでまだしばらくは大丈夫そうです。



プラグインの依存関係の設定

 さて、ついにいよいよプラグイン作成に入っていきます。

 Eclipseのプラグインを作る際には、とりあえず、まずはplugin.xmlの依存関係の設定をしてしまいましょう。最低限必要なのは以下の三つ

  1. org.eclipse.core.runtime
  2. org.eclipse.cdt.core
  3. org.eclipse.cdt.core.lrparser

f:id:nodamushi:20160728174346p:plain


UPCのgrammarディレクトリをコピー

 パーサーを作っていきたいとおもいますが、何も例がないところから作っていくのもしんどいです。というわけで、UPC Parserのgrammarを例として拝借します。このUPCもC99をベースに拡張して定義されています。
 cloneしたCDTのディレクトリのトップからupc/org.eclipse.cdt.core.parser.upc/grammarディレクトリをコピーしてきてください。

 grammarディレクトリの中はこうなっています。

  • build.xml: antファイル
  • parserBuild.properties: antの設定ファイル
  • upc: grammarファイルが入ったディレクトリ
    • UPCExpressionParser.g
    • UPCGrammarExtensions.g: 主に編集することになるファイル
    • UPCNoCastExpressionParser.g
    • UPCParser.g
    • UPCSizeofExpressionParser.g

 なお、名前がUPCのままではあれなので、私はSDCCに名称変更しました。

f:id:nodamushi:20160728174347p:plain

parserBuild.propertiesの修正

 parserBuild.propertiesにはLALR Parser Generatorの実行ファイルとテンプレートとなるlrparserのgrammarロケーション設定が3つ書かれています。自分が保存したフォルダと合うように修正してください。



build.xmlの修正

 まずは、build.xmlにあるupc,UPCの文字列を、今から作ろうとしている言語名に変えておいてください。生成されるクラス名にも対応してくるので、きちんと直しておいた方が良いと思います。私の場合は、upcはsdccに、UPCはSDCCに置換しました。

 次に以下の部分を自分が保存したフォルダの場所になるように修正してください。

<import file="../../org.eclipse.cdt.core.lrparser/grammar/generate.xml" />

 また、以下の一文を、先(↑)の修正した場所の前に挿入してください。(引数で渡すのが面倒くさいので、固定で設定ファイルを読ませるようにしただけです)

<property file="parserBuild.properties" />

 次に、次のパラメータを、これから作ろうとするパーサーのパッケージ階層と合うように修正してください。たとえば、私の場合はパッケージをnodamushi.internal.cdt.parser.sdccとしました。

<property name="upc_location" value="../src/org/eclipse/cdt/internal/core/dom/parser/upc" />


 で、locationとか設定したんだからここに結果を出力してくれるかと思ってたのですが、なぜかupcディレクトリにjavaファイルが出力されてしまいます。これを毎回手で移動するのが面倒くさいので、次の移動処理をtarget upcに追加しておいてください。これの後ろについでにclean_l_filesのタスクを呼び出す処理を追加しても良いかも。(中間生成物っぽいlファイルというのは、upc_locationに出力されるんだな、これが)

<move todir ="${upc_location}">
  <fileset dir="./upc"> <!-- ディレクトリ名はそれぞれの環境に合わせてください -->
    <include name="**/*.java"/>
  </fileset>
</move>

 SDCC用に作ったnodamushiの修正ファイルは、こちらを参照してください。build.xml




---Parser.gの修正

 続いては、upcディレクトリ(私の場合はsdccディレクトリ)のファイルを修正します。

 まずは、全てのファイル名の接頭辞UPCを、build.xmlでUPCを置換した文字列に変更します。(私の場合はSDCC)

 UPCGrammarExtensions.g(私の場合はSDCCGrammarExtensions.g)を除く以下のファイルの修正をします。

  • UPCExpressionParser.g
  • UPCNoCastExpressionParser.g
  • UPCParser.g
  • UPCSizeofExpressionParser.g

 これら全ファイルについて、の出力パッケージ名と、UPCGrammarExtensions.gのファイルをインクルードしている部分を修正します。「$Import C99~~~.g $End」となっているところは変更しないでください。

%options package=org.eclipse.cdt.internal.core.dom.parser.upc

↓ 修正後(build.xmlで指定したパッケージ階層と矛盾がないように)

%options package=nodamushi.internal.cdt.parser.sdcc
$Import
    UPCGrammarExtensions.g
$End

↓ 修正後(SDCCの部分は適宜変更してください)

$Import
    SDCCGrammarExtensions.g
$End


なお、UPCSizeofExpressionParser.gの$Importは以下のようになっていますが、$DropRulesの部分は消してください。拡張構文にsizeofに関する内容がある場合は、このように規則を削除するようです。SDCCにはこのような拡張構文はないので、詳しいことは調べていません。。。

$Import
    UPCGrammarExtensions.g
$DropRules

unary_expression
    ::= 'upc_localsizeof' '(' type_id ')'
      | 'upc_blocksizeof' '(' type_id ')'
      | 'upc_elemsizeof'  '(' type_id ')'
$End

↓ 修正後(SDCCの部分は適宜変更してください)

$Import
    SDCCGrammarExtensions.g
$End

GrammarExtensions.gの編集

 SDCCGrammarExtensions.g(UPCGrammarExtensions.g)の中身を編集していきます。ここに拡張構文のパーサーを記述していきます。

 GrammarExtendsions.gは$Defineパートと$Globalsパート、$Terminalsパート、$Rulesパートに分かれています。

  • $Define : テンプレートが使う変数の定義
  • $Globals : Javaのimport文を定義
  • $Terminals : 終端記号を定義
  • $Rules : 構文解析規則を記述

$Define

 $Defineパートでは、PraserActionクラス、ASTNodeFactoryクラス、SecondaryParserFactoryを指定します。といっても、今の段階では何も作っていないので、とりあえず、UPCの文字列を作る言語の名前に変えておけば良いと思います。実態は後で作ります。

$build_action_class /. SDCCParserAction ./
$node_factory_create_expression /. new SDCCASTNodeFactory() ./
$parser_factory_create_expression /. SDCCSecondaryParserFactory.getDefault() ./

 あと、2016年7月現在、このままの$Defineではコンパイルが通りません。
 とりあえず、コンパイルを通すという目的だけで、私は$Buildと$EndBuildという変数を上書き定義しておきました。正しいかどうかはともかく、こうしておけば、ひとまず通ります。

$Define
  $build_action_class /. SDCCParserAction ./
  $node_factory_create_expression /. new SDCCASTNodeFactory() ./
  $parser_factory_create_expression /. SDCCSecondaryParserFactory.getDefault() ./
  $Build /. action. ./
  $EndBuild /. ./
$End

 


$Globals

 次に$Globalsですが、importに何が必要かとか、今の段階ではわかりません。ひとまず空っぽにして、放っておききましょう。実際にSDCCParserActionなどを実装してから、最後に必要なものを足せばOKです。



$Terminals

 LALR Parser GeneratorではBNFを用いて構文を記述していきます。
 $Terminalsには、追加する終端記号を定義します。

 え?BNFって何?終端記号って何それおいしいの?って人は詳しくは、このページ(BNFおよび拡張BNF)Wikipediaなどを読んでください。あ、あと、Hatada's Home Page様の構文解析あたりも、とても有用な情報だと思います。

 ここではザックリと、構文に追加したいキーワードとだけ言っておきます。SDCCでは以下のものを定義しました。

$Terminals
  __data  __near  __xdata  __far  __idata  __pdata
  __code  __bit  __sfr  __sfr16  __sfr32  __sbit
  __at  __banked  __interrupt  __using  __reentrant
  __critical  __naked  __wparam  __shadowregs
  __preserves__regs  __asm  __endasm
$End

 なお、本当のキーワードと同じ名前でなくてかまいません。構文上、「--」はコメントと見なされてしまいますし、Javaの変数名に使えない文字列を使うことはできません。そういった場合は、何か別のわかりやすい名前をつけましょう。(C99のgrammarでは「...」はDotDotDotとか名前がついています。)



$Rulesの構文

 BNFに従って追加構文を記述していきます。BNFに追加されている特殊な構文について説明します。
 なお、BNFなにそれおいしいの?って人はこのページ(BNFおよび拡張BNF)Wikipediaなどを読んでください。


 1. rule ::= A | B | C というのは以下のように、ばらして書くことができるっぽいです。

rule ::= A
rule ::= B
rule ::= C

 2. 文法にマッチしたときに行うJavaの処理を、/. ./で括って定義することができます。(定義しなくても良いです)

rule 
  ::= 'HOGEHOGE'
   /.  System.out.println("終端記号HOGEHOGEにマッチした");  ./
    | 'MOGEMOGE'
   /.  System.out.println("終端記号MOGEMOGEにマッチした");  ./
    | rule1 rule2
   /. System.out.println("rule1の処理をした後、rule2の処理をし、この文字列がでる"); ./

 3. 文法的には単にrule ::= rule1 rule2なんだけど、rule1とrule2の間に特別にJavaの処理を挟みたい場合があります。そういう場合には、空にマッチする$emptyを使うことができます。このテクニックは<openscope-ast>で使います。

rule ::= rule1 rule1_2 rule2

rule1_2 ::= $empty
         /. System.out.println("1と2の狭間");

 

$Rulesに追加規則を定義する

 さて、ついにBNFで文法を追加していきましょう。Javaでの処理定義は、ひとまずBNFを作った後に考えれば良いです。

 追加する規則は、文法のルートとなる規則からたどれる必要があります。まぁ、基本的にはC99Grammar.gに定義されている規則の、どれかに追加すれば大丈夫です。

 じゃぁ、C99Grammar.gで定義されている規則は何があって、どこに追加すれば良いのか………それは………根性でC99Grammar.gを解析する以外ありませぇえん!!!!( ゚д゚)

 ま、まぁ、たぶん大丈夫ですよ。0からC言語の構文全部BNFで書き上げるよりは、ずっと解析するだけの方が簡単ですよ。実際、数時間でなんとか私はなったんですから、大丈夫です、きっと。UPCの例や私の作ったSDCCの例を見ながら、なんとなくできますよ。たぶん。

 後は試行錯誤です。

 一応、私がSDCCの拡張構文を追加するときに使った規則は以下です。

  • type_qualifier : 型の修飾関連の規則
  • simple_type_sqecifier_token : intやdoubleといった組み込み型の規則
  • function_direct_declarator : 関数名と引数の規則
  • statememt : 基本的にはブロックとかスコープが絡むようなものっぽい

 あと、C99で何が終端記号として宣言されているかは確認しておいた方が良いでしょう。私は、integer(整数)とidentifierの終端記号を利用しています。


 SDCCのために追加したBNF。(Javaの処理は省く)

-----------------------------------------------------------------------------------
-- Declarations
-----------------------------------------------------------------------------------
type_qualifier
    ::= address_space_name_qualifier
      | define_address
      | sdcc_type_qualifier


-- 以下二つを分けてる理由は特にない
address_space_name_qualifier
    ::= '__data'
      | '__near'
      | '__xdata'
      | '__far'
      | '__idata'
      | '__pdata'
      | '__code'
      | '__sfr'
      | '__sfr16'
      | '__sfr32'

sdcc_type_qualifier
   ::= '__banked'

define_address
    ::= '__at' '(' absolute_address ')'
      | '__at' absolute_address

absolute_address
    ::= 'integer'



simple_type_specifier_token
    ::= '__sbit'
      | '__bit'

-----------------------------------------------------------------------------------
-- Function
-----------------------------------------------------------------------------------

-- ↓C99Grammar.gからfunction_direct_declaratorをコピー
original_function_direct_declarator
    ::= basic_direct_declarator '(' <openscope-ast> parameter_type_list ')'
          /. $Build  consumeDirectDeclaratorFunctionDeclarator(true, true);  $EndBuild ./
      | basic_direct_declarator '(' ')'
          /. $Build  consumeDirectDeclaratorFunctionDeclarator(true, false);  $EndBuild ./


function_direct_declarator
  ::= original_function_direct_declarator sdcc_function_attributes


sdcc_function_attributes
 ::= sdcc_function_attribute
    | sdcc_function_attributes sdcc_function_attribute
 
sdcc_function_attribute
  ::= address_function_attribute
    | '__critical'
    | '__reentrant'
    | '__naked'
    | '__shadowregs'
    | '__wparam'
    | preserves_regs_attribute


address_function_attribute
  ::= '__interrupt' '(' absolute_address ')'
    | '__interrupt' absolute_address
    | '__using' '(' absolute_address ')'
    | '__using' absolute_address
    | '__interrupt'
    | '__using'

preserves_regs_attribute
    ::= '__preserves__regs' '(' ')'
      | '__preserves__regs' '(' preserves_regs_args ')'

preserves_regs_args 
    ::= preserves_regs_arg
    | preserves_regs_args ',' preserves_regs_arg

preserves_regs_arg
    ::= 'identifier'

-----------------------------------------------------------------------------------
-- Statements
-----------------------------------------------------------------------------------

statement
     ::= critical_statement
       | oldasm_satement

critical_statement
   ::= '__critical' compound_statement

oldasm_satement
   ::= '__asm' oldasm_contents '__endasm' ';'
     | '__asm' '__endasm' ';'


oldasm_contents
   ::= oldasm_content
     | oldasm_contents oldasm_content

oldasm_content
   ::= oldasm_item

-- __endasm以外全部
oldasm_item  ;;= 省略




 次回はJavaで何をどう処理するのかの部分を実装していきます。

Eclipse CDTにSDCCの方言を追加するプラグインを作った

 ScalaやらGoやらclojureやらKotlinといった華々しいモダン言語の世界とは真逆で、組み込み屋の言語発達は遅いです。アセンブリです。良くてCです。それもC88だったりすますデス(遠い目)。
 また、政治的理由によりドマイナーなマイコン使わされると、なんだか聞いたこともないようなドマイナーなコンパイラーを使わされることになります。
 このときに困るのが、エディタがそのマイナーコンパイラーの「方言」に対応していないことです。まぁ、個人的にはEmacsで良くね?と思いますが、そこも政治的いろいろな理由が絡むことがあります。


 そのマイナーなコンパイラの一つに、SDCCというC88のコンパイラーがあります。
 このSDCCの方言に対応したEclipse CDTの言語パーサー作ったのでご報告。

インストール

 新規ソフトウェアのインストールからサイト「https://nodamushi.github.io/nodamushi_sdcc/」を入力し、インストールしてください。たぶん、必須ライブラリも勝手に入ると思います。


設定方法

 プロジェクトの設定画面の「C/C++ General」→「Language Mappings」でContent typeが「C Source File」と「C Header File」に対してLanguage「SDCC」を割り当ててください。
 SDCCしか使わない、という人はプロジェクトごとに設定するのではなく、ワークスペースの設定のLanguage Mappingsで設定することもできます。

f:id:nodamushi:20160725130416p:plainf:id:nodamushi:20160725130415p:plain




使い方と注意点


 SDCCの方言をざっと挙げると、以下のようなものがあります。

//変数の配置場所を明示できる
__xdata __at(0xF800) unsigned char hoge;

//割り込み指示とかを書ける
void test() __interrupt __reentrant;

void test() __interrupt(1) __reentrant{
  //インラインアセンブラ(旧式)
	__asm
	nop
	nop
	nop
	__endasm;

	// critical block
	__critical{
	}
}


 この方言を単にEclipse CDT上で書こうものなら、以下のようにエラー警告地獄になります。

f:id:nodamushi:20160725130413p:plain


 今回私が作ったプラグインをLanguageに設定しておくと、こうなります。

f:id:nodamushi:20160725130414p:plain


 やったね!エラーが消えたよ!


 ただし、いくつかの注意点があります。

  1. エディタ上でエラーが出てなくても、コンパイルは通らないかもしれない
  2. __sbitなどはint型と解釈されてしまう
  3. __asm~__endasmの間は何も解析していない
  4. シンタックスハイライトの機能ではないので、__asm~~__endasmの中のコメント等も通常のCと同じように表示される
  5. __interruptなどの指示をした後に改行するとインデントが変


 1については、まぁ、一番重要なのは構文エラーじゃないところでエラーが出ることを解消することだったんで。。。特にC99を元にしてパーサーが作られているので、変数宣言をどこでやってもエラーにならないなど、実際のSDCCとは異なっています。ビルドしてからじゃないと問題がわかりません。SDCCにsyntax-onlyみたいなオプションがあれば、エラーチェックはそっちに任せられるんだけどなぁ。
 2については、現状諦めた。__sbitと__bitをBuiltin型に指定しようと努力はしたんだけど、良くわからんかった。3,4については、まぁ警告の黄色い線が出なくなっただけでもましと思ってください。

 5について何ですが、関数定義の後に、__interruptなどの指示をつけ、改行してから{を書くと以下のようになってしまいます。

void main()
{//<- ok
}

void test() __interrupt(1) __reentrant{
}//<= indent ok


void test1() __reentrant
		{ // <-  (´・д・`)
	int test;
		} // <-  (´・д・`)


 改行せずに{を書くと変にはならないので、改行しない書き方を標準としてください。






手動ダウンロード

手動でインストールしたい人はここからダウンロードしてください。
github.com

手動インストール方法:

  1. 依存ライブラリのC99 LR Parserを導入する
  2. 上記のダウンロードのところからzipを落としてきて、展開したものをdropinに突っ込む

1. C99 LR Parserのインストール

 まずは、C99 LR Parserが入っているかどうか確認してください。「help」→「Installation Ditails」の「Plug-ins」タブを選択し、「lrparser」で検索してください。インストールされている場合は、こんな感じになります。

f:id:nodamushi:20160725130417p:plain



 インストールがされていない場合は、「Help」→「Install New Software」でCDTのアップデートサイトを選択し、「CDT Optional Features」の中の「C/C++ C99 LR Parser」をインストールしてください。
f:id:nodamushi:20160725130418p:plain




2. SDCCパーサーのインストール

 ダウンロードしたzipを展開したものをEclipseが保存されているディレクトリにある「dropin」に入れる。

f:id:nodamushi:20160725134136p:plain