Cで書く中規模GUIアプリケーションから得た知見(初稿)

この記事は C言語 Advent Calendar 2016 の12日目の記事です。
11日目の前日はegtra氏の「配列でないオブジェクトに対するポインタ演算」でした。


@MNukazawaといいます。今、ベクターグラフィック・エディタをC言語で書いています。
Vecterion(べくてりおん)という名前です。
VecterionはGtk3/C言語で書いています。
現在進行形ですが、今日はVecterion開発の知見というか、Vecterionで使っているちょっと大きめのイディオムを、いくつか紹介したいと思います。

なおこの記事で言っている「中規模」ですが、
* ワンソース(ほぼmain.cだけ)に収まるアプリケーションを小規模
* 多人数で開発しなきゃ作れないアプリケーションを大規模
として、コマンドラインスクリプトよりは高度なことをしているけれど、コード量としては一人で収まる。
小規模と大規模の間くらい、程度の意味です。

小規模は、(語弊があるが)catやechoのような、ワンパスのコマンドラインスクリプト程度の機能。
大規模は、GIMP, LibreOfficeのようなGUIエディタや、Linuxカーネルなど。



なんというか、中規模コードの良い例を見つけることができなかったので書きました。
どなたかオススメのプロジェクトをご存知の方は教えてください。


開発中プロジェクトでまだ悩み中(本当は良くない)なので、まとまりきらない内容になっていますが、あなたが中規模プロジェクトを書く際に、一部でも参考になれば幸いです。

# ソース構成

## ソース構成

struct型の定義と一緒に、メンバ関数・構造体を操作するUtility関数を同じヘッダに置いている。
CヘッダとCソースは原則的に一対一対応させている。

これらのソース構成は、ビルド速度的には最善ではないが、関数の置き場所がわかりやすく、プロジェクト全体の読みやすさに繋がる。

"#define"でない方法で定数 を定義している。
===
 static const PvPoint PvPoint_Default = {0, 0};¬
===
static const は入れ子に宣言定義(?)できないので、その時は諦めて#defineを使う。


Gtk的な関数名の付け方を、そのままソースの名前にしている。
===
:~/etaion_vge$ ls include/
et_canvas.h             et_error.h             et_snap.h          pv_bezier.h           pv_io.h
et_canvas_collection.h  et_etaion.h            et_snap_panel.h    pv_cairo.h            pv_render_context.h
et_color_panel.h        et_key_action.h        et_state.h         pv_color.h            pv_render_option.h
et_define.h             et_layer_view.h        et_stroke_panel.h  pv_element.h          pv_renderer.h
et_doc.h                et_mouse_action.h      et_thumbnail.h     pv_element_general.h  pv_rotate.h
et_doc_history.h        et_mouse_cursor.h      et_tool_id.h       pv_element_info.h     pv_stroke.h
et_doc_history_hive.h   et_mouse_util.h        et_tool_info.h     pv_error.h            pv_svg_info.h
et_doc_id.h             et_pointing_manager.h  et_tool_panel.h    pv_file_format.h      pv_type.h
et_doc_manager.h        et_position_panel.h    pv_anchor_point.h  pv_focus.h            pv_vg.h
et_doc_relation.h       et_renderer.h          pv_appearance.h    pv_general.h

===



# コーディング

## ソース(関数・変数etc)のネーミング

sed, grep, vim, ctags, が効く名前を付けるべき。今はGtk3ライクな名前を付けている。
 vim上で一意性のある名前にするかは悩ましい(悩んでる)
 ctags対応には一意な名前を付ける。変に同名(static関数とか?)を付けなければ大丈夫。

特に開発中のプロジェクトは、実装により適切な名前が別にあることがわかったりするので、関数ローカルの一時変数名まで、置き換えを意識して付けるようにしている。
つまり、同名を避けることで、
 sed -i s/AA/BB/g */*.[hc]
 grep -r AA */*.[hc]

が効くように名前を付ける。
例えば、"Doc"を"Document"に直すときに、「ほぼ一括置換」くらいの手間で置き換えられる状態を目指している。
===
 11 struct EtDoc;¬
 12 typedef struct EtDoc EtDoc;¬
 13 ¬
 14 typedef int EtCallbackId;¬
 15 typedef void (*EtDocSlotChange)(EtDoc *doc, gpointer data);¬
 16 ¬
 17 ¬
 18 ¬
 19 EtDoc *et_doc_new();¬
 20 EtDoc *et_doc_new_from_vg(const PvVg *vg);¬
 21 void et_doc_delete(EtDoc *);¬
 22 EtDocId et_doc_get_id(EtDoc *self);¬
 23 char *et_doc_get_new_filename_from_id(EtDocId doc_id);¬

===



## C99(C11)を使う

変数の宣言、ただしgotoエラー処理と相性が悪い...
stdbool.hのbool型
const使う
安全な文字列操作 snprintf, strlcpy, g_strdup_printf
 strlcpy, g_strdup_printfは、それが無い環境では互換関数を書いたりしている。

# 未定義動作を避ける

最近は浸透してきた?
未定義動作の存在の認知度ってどのくらいなのだろう。
clang、本の虫、JPCERT CC、
 http://blog-ja.intransient.info/2011/05/c-13.html
 https://cpplover.blogspot.jp/2014/06/old-new-thing.html
 https://www.jpcert.or.jp/sc-rules/c-pre00-c.html

## ヘッダはシステム、自分ソースの順

ヘッダを書き間違えた時にとんでもないエラーメッセージが出ることがあるので、ヘッダはシステムヘッダを上に書く。
// と言いつつ、.h:.c一対一対応のコードはCソースの一番上に対応ヘッダファイルを書いているが。
===
  6 ¬
  7 #include <gtk/gtk.h>¬
  8 #include <gdk/gdk.h>¬
  9 #include <stdbool.h>¬
 10 #include "pv_element_general.h"¬
 11 #include "pv_color.h"¬
 12 #include "pv_stroke.h"¬
 13 #include "pv_appearance.h"¬
 14 ¬
===


## typedef enum定数

例えばアイコンをIDで管理して実体を引く関数を書くなら、get_icon_from_id(int icon_id)よりは(IconId icon_id)のほうが良いかと。
内部的にはint的なモノなのであくまで読みやすくするだけの糖衣。
コンパイラチェックが得られるとは期待しない。
===
 62 // ** ElementKind定数¬
 63 ¬
 64 typedef enum _PvElementKind{¬
 65 >-------PvElementKind_NotDefined,¬
 66 >-------/* special element document root */¬
 67 >-------PvElementKind_Root,¬
 68 >-------/* complex element kinds (group) */¬
 69 >-------PvElementKind_Layer,¬
 70 >-------PvElementKind_Group,¬
 71 >-------/* simple element kinds */¬
 72 >-------PvElementKind_Curve,¬
 73 >-------PvElementKind_Raster, /* Raster image */¬
 74 ¬
 75 >-------/* 番兵 */¬
 76 >-------PvElementKind_EndOfKind,¬
 77 }PvElementKind;¬
 78 ¬
===


## class的C記法 typedef struct & classメンバ的Utility関数

Gtkベースな、C++ classメンバなんて、C関数self引数の糖衣構文に過ぎない的思想。
最初はthisと名づけていたら、googletest側でC++予約語と衝突して、selfにリネームしなければならなくなった。
===
168 PvElement *pv_element_new(const PvElementKind kind)¬
169 {¬
170 >-------PvElement *self = (PvElement *)malloc(sizeof(PvElement));¬
171 >-------pv_assert(self);¬
172 ¬
173 >-------self->parent = NULL;¬
174 >-------self->childs = NULL;¬
175 ¬
176 >-------const PvElementInfo *info = pv_element_get_info_from_kind(kind);¬
177 >-------pv_assertf(info, "%d", kind);¬
178 >-------pv_assertf(info->func_new_data, "%d", kind);¬
179 ¬
180 >-------self->data = info->func_new_data();¬
181 >-------pv_assertf(self->data, "%d", kind);¬
182 ¬
183 >-------self->color_pair = PvColorPair_Default;¬
184 >-------self->stroke = PvStroke_Default;¬
185 ¬
186 >-------self->kind = kind;¬
187 ¬
188 >-------self->etaion_work_appearances = pv_appearance_parray_new_from_num(NUM_WORK_APPEARANCE + 1);¬
189 >-------pv_assert(self->etaion_work_appearances);¬
190 ¬
191 >-------return self;¬
192 }¬
193 ¬
===


## 分岐の方法

switch構文が読みやすくて好き > VimのデフォルトとLinuxでインデントルール衝突しているがまあそれはそれ。
switch文は縦横サイズを食うので、サンプルコードは略す。

## Info型とget_info_from_kind()関数

不満はあるが、Info型をKind,Id,Indexのいずれかで引く方式を使っている。
記事参照 「Cに欲しい機能 インデックス番号付き構造体配列」
===
1739 ¬
1740 const PvElementInfo _pv_element_infos[] = {¬
1741 >-------{PvElementKind_Root, "Root",¬
1742 >------->-------.func_new_data>->------->------->-------= _func_group_new_data,¬
1743 >------->-------.func_free_data>>------->------->-------= _func_group_free_data,¬
1744 >------->-------.func_copy_new_data>---->------->-------= _func_group_copy_new_data,¬
1745 >------->-------.func_write_svg>>------->------->-------= _func_group_write_svg,¬

1757 >------->-------.func_get_rect_by_anchor_points>>-------= _func_notimpl_get_rect_by_anchor_points,¬
1758 >------->-------.func_set_rect_by_anchor_points>>-------= _func_notimpl_set_rect_by_anchor_points,¬
1759 >------->-------.func_get_rect_by_draw>->------->-------= _func_notimpl_get_rect_by_draw,¬
1760 >------->-------.func_apply_appearances>>------->-------= _func_nop_apply_appearances,¬
1761 >-------},¬
1762 >-------{PvElementKind_Layer, "Layer",¬

===

## データ構造の生成というか確保というか

malloc,freeで生成・削除し、ポインタで保持する。
構造体の中身を見せたくなければ、Cのimplイディオムのように、ヘッダに宣言のみ書く方法で定義を隠す。

可変長配列は、ポインタ配列をポインタポインタで確保して使うことで実現している。
Cを使っているので、可変長配列が使いたければmalloc,freeと仲良くするしかない(mallocの速度については必要になるまでは忘れる)。

 size_t *_get_parray_num()トリックで個数を取るとイテレートがやりやすい。
 for(int i = 0; i < (int)num; i++) が定型文になりつつある。
===
 170 >-------int num = pv_general_get_parray_num((void **)elements);¬
 171 >-------for(int i = 0; i < num; i++){¬
 172 >------->-------const PvElement *element = elements[i];¬
 173 >------->-------const PvElementInfo *info = pv_element_get_info_from_kind(element->kind);¬
 174 >------->-------et_assertf(info, "%d", element->kind);¬
 175 ¬
 176 >------->-------PvRect rect = info->func_get_rect_by_anchor_points(element);¬
 177 ¬
 178 >------->-------if(0 == i){¬
 179 >------->------->-------rect_extent = rect;¬
 180 >------->-------}else{¬
 181 >------->------->-------rect_extent = pv_rect_expand(rect_extent, rect);¬
 182 >------->-------}¬
 183 >-------}¬
 184 ¬
 185 ¬
 

===

## エラー返り値

NULLまたはbool falseに統一。
それ以外が必要なら最終引数に "~bool *is_error)"
引数が不正な場合、Vecterionはabort()するが、PhotonVectorはエラーを返す方針。
===

  8 #define et_assert(hr) \¬
  9 >-------do{ \¬
 10 >------->-------if(!(hr)){ \¬
 11 >------->------->-------fprintf(stderr, "et_assert: %s()[%d]:'%s'\n", __func__, __LINE__, #hr); \¬
 12 >------->------->-------assert(hr); \¬
 13 >------->-------} \¬
 14 >-------}while(0);¬
 15 ¬
 16 #define et_assertf(hr, fmt, ...) \¬
 17 >-------do{ \¬
 18 >------->-------if(!(hr)){ \¬
 19 >------->------->-------fprintf(stderr, "et_assertf: %s()[%d]: "fmt"\n", \¬
 20 >------->------->------->------->-------__func__, __LINE__, ## __VA_ARGS__); \¬
 21 >------->------->-------assert(hr); \¬
 22 >------->-------} \¬
 23 >-------}while(0);¬
 24 ¬
 25 // Caution: depend gcc¬ 33 #define et_error(fmt, ...)  \¬
 34 >-------fprintf(stderr, "error: %s()[%d]: "fmt"\n", __func__, __LINE__, ## __VA_ARGS__)¬ 37 #define et_debug(fmt, ...)  \¬
 38 >-------fprintf(stdout, "debug: %s()[%d]: "fmt"\n", __func__, __LINE__, ## __VA_ARGS__)¬
 

===


...という感じです。
本当はもっといろいろありますし、どうしてこれが良さそうか、という理由も、いずれ書こうと思っています。
また、ここに書いたのとは別に Vim (その2) Advent Calendar 2016 にVimでCアプリケーションを開発する話を書く予定です。そちらもよろしくお願いします。

この記事は C言語 Advent Calendar 2016 の12日目の記事でした。
13日目の明日はyashi氏の「The Meson Build System」です。楽しみですね。

このブログの人気の投稿

squid3プロキシサーバの設定(Ubuntu13.10)

Ubuntu13.04で使える動画編集ソフト一覧

GIMP2.8でイラストにペン入れを行う