GNOME Planner for Windows 独自ビルド版

リポジトリ構成を変更することにしたので一時公開停止中。

最近ちょいとプロジェクト管理ツールが使いたくなった。プロジェクト管理ツールと言えばMicrosoft Projectが有名だが、如何せん値段が高いし、そこまで高機能である必要はない。代わりのツールを色々検索してみたが、この手の作業はブラウザでやるのが流行りのようで、多くはWebアプリになっている。できればオフラインで使えて、かつ余計なもの(Javaランタイムとか)を入れなくて良いのがいい。あと、インストーラなしでフォルダコピーだけで使えるともっといい。

そうなると候補になるのはGNOME Plannerくらい。ただ、公式のWindows版を試したところファイル書き込み時にクラッシュする問題があって使えない。公式のメーリングリストにも似たようなレポートが来ているが、まだ修正されてないようだ。というより、ここ最近は各国語の翻訳に関するコミットはあるものの、ソースコードの修正はほとんど行われていないようだ。

ということで、自分で直してみたら一応動かすことに成功。

f:id:wagavulin:20170121221653p:plain

せっかくなのでGitHubに上げたので使ってみて欲しい。

ダウンロード

ダウンロードはGitHubのリリースページから。なお、ソースコードはここ

インストールと起動

インストールはZipファイルを展開するのみ。展開したフォルダ中のbin/planner.exeを起動すれば使えるはず。インターネット上からダウンロードしたexeを実行しようとすると多分メッセージが出るが、元のコードから余計な機能を加えたりはしてないので大丈夫。もちろんそれを信じるかは各人の判断だが。

制限

とりあえず動くようにしただけなのでまだ色々制限はある。

  • UIは英語のみ
  • 日本語の入力はできるが、IMEのウィンドウが別途画面左上に表示されるので見にくい
  • ファイル名・パスに日本語があると開けない
  • ユーザガイドは入ってない
  • ビルドオプションで指定できる機能はほぼすべてオフ(pythonプラグインやデータベースサポートなど)
  • インストーラはなくファイルの関連付けはしないので、.plannerファイルのダブルクリックで開くことはできない

そのうち直そうと思うが、現状でもとりあえず使う分にはそんなに困らないと思う。

バグ報告など

自分の環境(Windows 10 64bit)でしか動作チェックしていないので他の環境で動くかは分からない。もし問題があったらこの記事のコメントかGitHubのIssuesにでも挙げて欲しい。まあ手元にない環境での問題は多分直せないが。

あと、無事動いた場合は「Windows 7で動いた」などというだけでも助かるのでコメントしてもらえればと思う。

Bash on Windows (Windows Subsystem for Linux) でvalgrindを動かす

Windows上でLinux(Ubuntu)のバイナリを動かすBash on Windowsが発表され(Windows Subsystem for Linuxという呼び方もされているが、用語の使い分けがよく分からない)、bash, gcc, clangといったツールがWindows上で動くようになった。

gccやclangを動かすだけなら今までもCygwinやMSYSといった環境があったが、これらの環境にはvalgrindを動かすことができないという大きな問題がある。メモリリークのチェックなら他にもいくつかツールはあるが、やはりvalgrindが優秀だと思う。

Bash on WindowsならLinuxのシステムをかなり忠実に再現していて、Ubuntuのバイナリがそのまま動いているらしい。であればvalgrindもいけるはず、ということで試してみた。

インストールはapt-get install valgrindで簡単にできる。で動かしてみたところ、

$ valgrind --leak-check=full ./a.out
--15035:0:aspacem   -1: ANON 0038000000-00383d5fff 4022272 r-x-- SmFixed d=0x000 i=421087  o=0       (0) m=0 /usr/lib/valgrind/memcheck-amd64-linux
--15035:0:aspacem  Valgrind: FATAL: aspacem assertion failed:
--15035:0:aspacem    segment_is_sane
--15035:0:aspacem    at m_aspacemgr/aspacemgr-linux.c:1502 (add_segment)
--15035:0:aspacem  Exiting now.

というエラーを吐いて終了。a.outコマンドを実行するのに失敗しているという分けでもなく、valgrind --helpだけでも落ちる。

ググってみたところ、bash on windowsのGithubによれば、valgrind-3.11.0をソースから入れれば動くらしい。

ということでやってみた。

ソースのダウンロードは公式サイトのダウンロードページから。ビルドは特に難しいことなく、configure; make; make installでできるっぽい。aptで入れたvalgrindとぶつからないよう、--prefixオプションを使おう。

$ tar xvjf valgrind-3.11.0.tar.bz2
$ cd valgrind-3.11.0
$ ./configure --prefix=/usr/local/valgrind-3.11.0
$ make -j4
$ sudo make install

これだけでビルド・インストールできたので、早速メモリリークを起こすサンプルを作って試したところ……

$ /usr/local/valgrind-3.11.0/bin/valgrind --leak-check=full ./a.out
==15064== Memcheck, a memory error detector
==15064== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==15064== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==15064== Command: ./a.out
==15064==
==15064==
==15064== HEAP SUMMARY:
==15064==     in use at exit: 1 bytes in 1 blocks
==15064==   total heap usage: 1 allocs, 0 frees, 1 bytes allocated
==15064==
==15064== 1 bytes in 1 blocks are definitely lost in loss record 1 of 1
==15064==    at 0x4C2B145: operator new(unsigned long) (vg_replace_malloc.c:333)
==15064==    by 0x40071E: main (main.cpp:9)
==15064==
==15064== LEAK SUMMARY:
==15064==    definitely lost: 1 bytes in 1 blocks
==15064==    indirectly lost: 0 bytes in 0 blocks
==15064==      possibly lost: 0 bytes in 0 blocks
==15064==    still reachable: 0 bytes in 0 blocks
==15064==         suppressed: 0 bytes in 0 blocks
==15064==
==15064== For counts of detected and suppressed errors, rerun with: -v
==15064== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1)

できた!

これでBash on WindowsでのC/C++の開発も結構いけるんじゃないだろうか。

ちなみに、valgrindのビルドにかかった時間(make -j4の時間)は、Windows 10 (64bit)、Core i7-2600 (3.4GHz)、RAM 16GB、SSDの環境で約47秒。また、このPCのVMwareの仮想環境に入れたUbuntu-16.04 (64ビット)の環境では、make -j4で1分12秒かかった。それぞれ1回しかやっていないので正確な計測ではないが、Bash on WindowsのC言語開発環境のパフォーマンスはVMware上のLinuxと比べてもそれほど大差はないのかもしれない。

GCC7ではエラーメッセージが改善されるらしい

プログラムを書いているときには当然色々ミスをするが、GCC(gcc/g++)が出すメッセージは Clang(clang/clang++)に比べて分かりにくい/不親切なことが多い。

例えば、FooBarという名前のクラスが宣言されていて、それを使おうとしたときに以下のように間違ってFooBazと書いたとする。

int main(int, char**){
    FooBaz foobar;
}

g++-5.4.0でコンパイルした場合のメッセージは以下のようになる。

g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:9:5: error: 'FooBaz' was not declared in this scope
     FooBaz foobar;
     ^

最新のg++-6.2では、エラーの位置を示す'^'が波線(~~~~~~)になるが、内容は特に変わらない。

/usr/local/gcc-6.2.0/bin/g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:9:5: error: 'FooBaz' was not declared in this scope
     FooBaz foobar;
     ^~~~~~

clang++-3.8では以下のように修正候補を挙げてくれる。

clang++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp:9:5: error: unknown type name 'FooBaz'; did you mean 'FooBar'?
    FooBaz foobar;
    ^~~~~~
    FooBar
main.cpp:3:7: note: 'FooBar' declared here
class FooBar {
      ^
1 error generated.

あるいは、以下のように文末のセミコロンを忘れた場合、

int main(int, char**){
    FooBaz foobar
    foobar.method1();
}

g++-5.4、g++-6.2、clang++-3.8でコンパイルした結果はそれぞれ以下のようになる。

# g++-5.4
g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:10:5: error: expected initializer before 'foobar'
     foobar.method1();
     ^

# g++-6.2
/usr/local/gcc-6.2.0/bin/g++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:10:5: error: expected initializer before 'foobar'
     foobar.method1();
     ^~~~~~

# clang++-3.8
clang++ -g -Wall -Wextra   -c -o main.o main.cpp
main.cpp:9:18: error: expected ';' at end of declaration
    FooBar foobar
                 ^
                 ;
1 error generated.

g++では、セミコロンを忘れた行の次の行がエラーとなっているが(普通に解析したらそうなるだろう)、clang++ではちゃんと「セミコロンがない」というメッセージになっている。

これがClangを使う一つの理由だったのだが、Phoronixの記事によれば、GCC7ではエラーメッセージか改善され、タイプミスに対する修正のヒント(Fix-it hints)を出すようになるらしい。

また、その場所にアンダーラインが引かれる、セミコロンがないことに関するFix-it hintsを出す、などと書かれているで、多分Clangと似たような感じになるのだろう。

一昔前まで、フリーで使えるC/C++コンパイラと言えばGCC一択だったところにClangの登場し、GCCにない機能が色々使えるようになったが、それに刺激されたGCCも改善が進んでいるようだ。

valgrindが検出するメモリリークの種類

C/C++のメモリリークチェックツールと言えばvalgrindが有名だが、メッセージの詳細を日本語で説明しているサイトはあまりないようなので調べてみた。

valgrindは、mallocやnewで確保されたメモリ領域がポインタによって辿れるかどうかをチェックすることでメモリリークを検出する。以下の例を見てみよう。

char *g_ptr;

void func(){
    char *p = new char[32];
    // (A)
}

int main(){
    func();
}

main関数から始まり、そこからfuncが呼ばれ、現在 (A) の位置にいるとしよう。このとき、メモリ上にはmain関数とfunc関数で定義された変数があり、また、グローバル変数であるg_ptrも有効な状態で存在している。func関数が抜けると変数pは無効になる。

このpやg_ptrのようなデータのことをroot-setと呼ぶ。valgrindの公式マニュアルにある公式な定義では、root-setは以下の2つからなる。

  • general purpose registers of all threads(全スタックの汎用レジスタ)
  • initialised, aligned, pointer-sized data words in accessible client memory, including stacks(スタックを含むアクセス可能なクライアントメモリにある、初期化された、整列された、ポインタサイズのデータワード)

この説明だと分かりにくいが、実際には

  • グローバル変数や関数内static変数などの静的データ
  • スタック中のデータ
  • レジスタ中のデータ

ということだと思う。また、メモリリークのチェックはプログラム終了時の状態を見るものなので、つまりmain関数も終わった状態なので、スタック領域は基本的にはない(実際にはmain関数が最後の関数というわけではないが、一般的なプログラムソースコードに書かれているような自動変数はすべてなくなっている)。

valgrindはプログラム終了時に、ヒープ領域中に確保されたブロックがroot-setから辿れるかどうかをチェックすることでメモリリークを検出する。また、その状況によって以下の4種類に分類される。場合によっては修正する必要がないケースもある。

  • definitely lost
  • indirectly lost
  • possibly lost
  • still reachable

definitely lost

definitely lostは、ヒープ中のブロックを参照するポインタがroot-set上にないということを意味する。以下に簡単な例を上げる。

int main(int, char**){
    char* p = new char[128];
}

このmain関数の中でnewによってヒープ領域に128バイト確保され、ポインタpはその領域へのポインタとなる。pはmain関数で定義されたもので、スタック領域に確保されている。そのため、main関数が終わればpは削除される。その結果、newで確保された領域は解放されないまま、どこからも指されていない状態になる。このような領域はdefinitely lostとなる。

main関数実行中
 root-set     Heap
+-------+    +--------------------------------------+
|   p---------> [128バイトデータ]                   |
+-------+    +--------------------------------------+
main関数終了後
 root-set     Heap
+-------+    +--------------------------------------+
|       |    |  [128バイトデータ]                   |
+-------+    +--------------------------------------+

indirectly lost

indirectly lostは、そのブロックを参照するポインタはあるがroot-setからは辿れない状態を指す。以下に例を挙げる。

class B {
};

class A {
public:
    A() : m_b(new B) {}
    ~A() {
        delete m_b;
    }
private:
    B* m_b;
};

int main(){
    A* a = new A;
}

この例では、クラスAをnewで確保し、さらにAのコンストラクタでクラスBを確保している。その結果、メモリの状態は以下のようになる。

 root-set     Heap
+-------+    +---------------------------------+
|   a---------> [A instance] ---> [B instance] |
+-------+    +---------------------------------+

main関数が終了し、ポインタaがなくなると、以下のようになる。

 root-set     Heap
+-------+    +---------------------------------+
|       |    |  [A instance] ---> [B instance] |
+-------+    +---------------------------------+

ここで、BインスタンスはAインスタンスが参照されているが、参照元を辿っていってもroot-setには辿りつけない。つまり、root-setからは辿れない。この状態をindirectly lostと呼ぶ。一方インスタンスAはどこからも参照されないのでdefinitely lostとなる。

indirectly lostもたいていの場合は修正するべきものだが、まずはdefinitely lostの修正を優先するのが良いだろう。というのも、indirectly lostの領域の解放は、それを参照している参照元インスタンスの責任である場合があるからだ。そのため、valgrindはデフォルトではindirectly lostについては詳細を表示しない。

この例でもAのデストラクタでBも解放するようになっているため、main関数でaをdeleteすればBも解放される。そのため、「Bインスタンスを解放する」という修正を直接行う必要はない。

一方、もしAのデストラクタがBを解放しない場合、aをdeleteするとBインスタンスだけが残る。

 root-set     Heap
+-------+    +------------------------------------+
|       |    |                       [B instance] |
+-------+    +------------------------------------+

すると今度はBインスタンスがdefinitely lostになるので、改めて修正方法を検討しよう。

still reachable

プログラム終了時にヒープ領域にデータが残っているが、それがroot-setから辿れる状態にあることをstill reachableと呼ぶ。以下に例を挙げる。

char* g_ptr1;

int main(){
    g_ptr1 = new char[128];
}

この例では、newで確保された領域がグローバル変数g_ptr1に参照されたままになる。このような状態はstill reachableとなる。

still reachableもデフォルトでは詳細は表示されない。still reachableは修正する必要がない場合が多い。例えばグローバル変数にシングルトンインスタンスとして使うようなデータが保持されている場合、必ずしもそれを解放する必要はないだろう。

possibly lost

possibly lostは、malloc/newで確保された領域の内の先頭以外のアドレスが参照されている場合に起こる。以下に例を挙げる。

char* g_ptr2;

int main(){
    char* p = new char[128];
    g_ptr2 = &p[10];
}

この例ではnewで確保された領域の先頭をpが参照しているが、g_ptr2はその領域の途中を指している。

 p   g_ptr2
 |    |
 v    v
 +----------------------+
 |Heap area             |
 +----------------------+

main関数が終わるとpはなくなり、g_ptr2だけが残る。すると、確保された領域の先頭ではない場所を参照するポインタのみが残る。このような状態をpossibly lostと呼ぶ。

possibly lostは一般に修正するべきものであり、valgrindもデフォルトで詳細を表示する。上に挙げた例は恣意的に起こした例だが、公式にマニュアルにはpossibly lostになる例がいくつか挙げれられている。一例としては、いくつかのstd::stringの実装は、確保された領域の先頭3ワードは文字列の長さ、領域のサイズ、参照カウントに使われるため、文字列データへのポインタは確保領域の途中から始まるらしい。

メッセージの表示・抑制

valgrindでメモリリークのチェックをするなら、--leak-check=fullを付けよう。でないとリークした領域を確保したソースコード位置など、修正に必要な情報が表示されない。

また、デフォルトでは詳細情報についてはdefinitely lostとpossibly lostしか表示されない。indirectly lostやstill reachableを表示するには--show-leak-kindsオプションを使う。引数はdefinite、indirect、possible、reachableをカンマ区切りで繋げる。また、すべて表示したい場合はallとする。

 # indirectly lostとstill reachableのみ表示する
 $ valgrind --leak-check=full --show-leak-kinds=indirect,reachable prog-name
 # すべての種類について表示する
 $ valgrind --leak-check=full --show-leak-kinds=all prog-name

特定のリークを無視したい場合

特定のリークを無視したい場合(誤検出である場合、依存ライブラリの問題であり自分では修正できない場合など)、表示を抑制することもできる。"valgrind suppressions"で調べれば色々出てくると思う。

まとめ

  • リークの種類の違いを理解しよう
    • definitely lostは修正するべき。
    • possible lostも修正するべき。
    • indirectly lostは直接修正するのではなく、definitely lostを直そう。それでダメならdefinitely lostに変わっているだろう。
    • still reachableは多くの場合修正する必要はない。
  • 必要に応じて--show-leak-kindsオプションを使って不要なメッセージを抑制しよう。
  • 特定の箇所に関する表示を抑制することもできる。