Self-taught KNF(Kernel Normal Form) Style Guide

概要

以下のコーディングスタイルはカーネルなどの低レベルコーディングに加え、ユーザースペースにおけるコーディングにも及ぶ。 OpenBSD/KNF、FreeBSD/KNF、Apple kernel programing styleからいい部分を抜粋し、個人的に違うなと思った部分や、 C23のモダンな機能も追加したオレオレCモダンコーディングスタイルである。

C言語の規格

C言語の規格はC23以上で記述することとする。最低でもC11以上を使用することを推奨する。間違ってもC99や、C89を使ってはならない。 また、処理系は

  • GNU GCC 13以上
  • LLVM 19-init以上
  • が条件である。 また、GCC13の場合は-std=c23ではなく、c2xであることに注意。

    コメント・ドキュメント

    C++の"//"というスタイルではC言語では使わない。以下のようになる。また、日本語は使ってはならない。いかなる理由があっても、英語でコメントをすること。また、複雑でわかりにくい関数に関して、カーネルの記述でのみASCIIアートを使ってメモリの図を使ってコメントしても構わない。

    
    	/*
    	 * A very important one-line comment would be as follows. 
    	 */
    
    	/* Most one-line comments look like this. */
    
    	/*
    	 * This is what a multi-line comment looks like. Make it a real sentence.
    	 * Fill it in so that it looks like a real paragraph. 
    	 */
    	    

    ヘッダーファイルの扱いについて

    カーネル・インクルード・ファイル(<sys/*.h>)が最初にインクルードされる。 <sys/types.h>をインクルードする際、<sys/cdefs.h>をインクルードする必要はない。(OpenBSD/NetBSD/FreeBSD/macOS) また、<>内は非ローカル・インクルードである。clangの場合は<>にローカル・インクルードがあっても大丈夫だが、gccの場合、弾かれる。 また、具体的なインクルードの順番として、<sys/param.h>または<sys/types.h>が最初に来る。<sys/types.h>には<sys/param.h>が含まれる。 その次に必要であれば<sys/system.h>をインクルードする。そのあとは、アルファベット順に並べる。

    
    	#include	<sys/types.h>  /* Non-local includes in	angle brackets.	*/
    	#include	<sys/systm.h>
    	#include	<sys/endian.h>
    	#include	<sys/lock.h>
    	#include	<sys/queue.h>
    	    
    ネットワークプログラムがある場合、それはカーネル・インクルード・ファイルの2番目にネットワーク・インクルード・ファイルを置く。
    
    	#include <net/if.h>
    	#include <net/if_dl.h>
    	#include <net/route.h>
    	#include <netinet/in.h>
    	#include <arpa/inet.h>
    	    

    次に空白行があり、その後に/usr/includeファイルが続く。絶対に/usr/includeの前にカーネルやネットワークなどのインクルード・ファイルをインクルードしない。 グローバルパス名は<paths.h>で定義されている。 プログラムにローカルなパス名はローカルディレクトリの "pathnames.h" にある。

    
    	#include <paths.h>
    	    

    ローカル・インクルード・ファイルの前にもう一つ空白を入れる。

    
    	#include	"pathnames.h"	       /* Local	includes in double quotes. */
    	    

    ヘッダーファイル内では、関数のプロトタイプを __BEGIN_DECLS と __END_DECLS の対応するペア内に配置すること。これにより、ヘッダーファイルがC++から使用可能になる。

    プライベート/パブリック 関数の扱い

    プライベート関数のプロトタイプ(すなわち、他で使用されていない関数)は、最初のソースモジュールの先頭に配置する。 カーネルでは、プライベート関数は使用される前に定義されていればプロトタイプは必要ない。 ユーザースペースでは、1つのソースモジュールにローカルな関数は 'static' で宣言すべきである。 ただし、これはカーネルでは行われるべきではありません。なぜなら、それが行われるとカーネルデバッガの使用が不可能になるからである。

    他のファイルから使用される関数は、関連するヘッダーファイルでプロトタイプ宣言さ 1つのモジュールで複数回使用される関数は、別のヘッダーファイルにまとめられる。例えば、"extern.h" のようなものである。 プロトタイプには型に関連する変数名を持たせてはいけない。すなわち、

    
    	GOOD:
    	void	function(int);
    
    	BAD:
    	void	function(int a);
    	    

    プロトタイプには、関数名を整列させるためにタブの後に余分なスペースを入れても構わない。

    
    	static int    add_numbers(int num1, int num2);
    	static double calculate_average(const double *numbers, int count);
    	    

    関数名と引数リストの間にスペースを入れないこと。

    
    	GOOD:
    	void function(int arg1, char arg2);
    
    	BAD:
    	void function (int arg1, char arg2);
    	    

    マクロの使い

    実装名前空間では#defineや宣言を行わない。これはアサートの実装、または過去のコードの保守でのみ使用する。 "unsafe" マクロ(副作用を持つもの)および定数のマクロの名前は全て大文字であるべき。 式のようなマクロの展開は、単一のトークンであるか、外側に括弧があるべき。 #define とマクロ名の間にはスペースまたはタブ文字を入れるが、ファイル内で一貫性を保つ。 マクロが関数のインライン展開である場合、関数名は全て小文字で、マクロは同じ名前を全て大文字で持つ。 バックスラッシュは右寄せにし、可読性を向上させる。 マクロが複合文をカプセル化する場合は、それをdoループで囲むことで、if文で安全に使用できるようにする。 最終的なセミコロンはマクロではなく、呼び出しで提供する。これはpretty-printersやエディタでの解析を容易にする。

    
    	#define MACRO(x, y) do {                      \
    		variable = (x) + (y);                     \
    		(y) += 2;                                 \
    	} while (0)
    	    

    C23以上または新しいコードでは、constexprを使用する。defineは計算を行わず、コンパイル時にただ展開するだけだが、C23で追加されたconstexpr はコンパイル時に一回計算されるため、未定義動作が起こりにくい。

    
    	constexpr float f = 23.0f;
    	constexpr float g = 33.0f;
    	constexpr float h = f / g; // is not affected by fesetround() above
    	printf("%f\n", h);
    	    

    ただし、以下の条件下でconstexprを使用することはできない。

  • ポインタ(ヌルポインタは可)
  • 可変修飾型
  • アトミック型
  • volatile型
  • restrictポインタ
  • 条件付きでコードがコンパイルされる場合、#ifdefまたは#ifが使用されると、対応する #endif または #else の後にコメントが追加されることがる。 これは読者が条件付きでコンパイルされたコード領域がどこで終わるかを簡単に判別できるようにするためである。 このコメントは、(主観的に)長い領域や20行を超える領域、または入れ子になった一連の #ifdef が読者を混乱させる可能性がある場合にのみ使用されるべきである。 コメントは #endif または #else から単一のスペースで区切られるべきである。 短い条件付きコンパイル領域の場合、閉じるコメントは使用しないこと。

    #endif のコメントは、対応する #if または #ifdef で使用された式と一致するべべきである。 #else および #elif のコメントは、前の #if および/または #elif ステートメントで使用された式の否定と一致するべきである。 コメントでは、サブエクスプレッション "defined(FOO)" は "FOO" と略される。 コメントの目的で、"#ifndef FOO" は "#if !defined(FOO)" として扱われる。

    
    	#ifdef KTRACE
    	#include <sys/ktrace.h>
    	#endif
    
    	#ifdef COMPAT_43
    	/* A large region here, or other	conditional code. */
    	#else /*	!COMPAT_43 */
    	/* Or here. */
    	#endif /* COMPAT_43 */
    
    	#ifndef COMPAT_43
    	/* Yet another large region here, or other conditional code. */
    	#else /*	COMPAT_43 */
    	/* Or here. */
    	#endif /* !COMPAT_43 */
        

    列挙型、構造体の扱い

    列挙型の値はすべて大文字である。

    
    	enum enumtype { ONE, TWO } et;
    	    

    識別子においては、キャメルケースやタイトルケースではなく、スネークケースの使用が好まれる。

    変数を構造体で宣言する際には、使用目的、次にサイズ(大きい順から小さい順)、そしてアルファベット順にソートして宣言すること。 通常、最初のカテゴリは適用されないが、例外もある。それぞれに独自の行を使用する。 最初の単語の後にはタブを挿入し、例えば 'int^Ix;' や 'struct^Ifoo *x;' のようにする。(注:^Iはタブ文字を表している。)

    メジャーな構造体は、それらが使用されるファイルの先頭に宣言するか、複数のソースファイルで使用される場合は別個のヘッダーファイルに宣言する。 構造体の使用は別個の宣言で行い、ヘッダーファイルで宣言されている場合は "extern" を使用する。

    
    	struct foo {
    		struct foo *next;      /* List of active foo */
    		struct mumble amumble; /* Comment for mumble */
    		int bar;
    	};
    
    	struct foo *foohead; /* Head of global foo list */
    	    

    可能であれば、独自のリストを実装する代わりに、queue(3) マクロを使用すること。したがって、前述の例は以下のように書き直す方が良い。

    
    	#include <sys/queue.h>
    
    	struct foo {
    		LIST_ENTRY(foo) link; /* Queue macro glue for foo lists */
    		struct mumble amumble; /* Comment for mumble */
    		int bar;
    	};  
    
    			LIST_HEAD(, foo) foohead; /* Head of global foo list */
    	    

    構造体の型に対して typedef を使用することは避けること。 これにより、アプリケーションが通常の struct タグを使用して構造体へのポインタを不透明に使用することが可能で、これは通常、ポインタを使った操作が可能であり、有益である。 typedef が必要な場合は、その名前を struct タグと一致させるようにすること。 また、"t" で終わる typedef は避け、Standard C または POSIX で指定されている場合を除く。

    実装への具体的なアプローチ

    
    	/*
    	* All major routines should have a comment briefly describing what
    	* they do. The comment before the "main" routine should describe
    	* what the program does.
    	*/
    	int
    	main(int argc, char *argv[])
    	{
    	    int aflag, bflag, ch, num;
    	    const char *errstr;
    	    

    一貫性のために、オプションの解析には getopt(3) を使用する。 オプションは getopt(3) の呼び出しと switch 文でソートされrうべきである。 switch のパーツが連鎖する場合を除き、FALLTHROUGH コメントが必要である。 数値引数は正確性を確認する必要がある。

    
    		while ((ch = getopt(argc, argv, "abn:")) != -1) {
    			switch (ch) { /* Indent the switch. */
    				case 'a': /* Don't indent the case. */
    					aflag = 1;
    					/* FALLTHROUGH */
    				case 'b':
    					bflag = 1;
    					break;
    				case 'n':
    					num = strtonum(optarg, 0, INT_MAX, &errstr);
    					if (errstr) {
    						warnx("number is %s: %s", errstr, optarg);
    						usage();
    					}
    					break;
    				default:
    					usage();
    			}
    		}
    			
    		argc -= optind;
    		argv += optind;
    	    

    キーワードの後にスペースを使用すること(if、while、for、return、switch)。 ゼロまたは単一のステートメントの制御文には、そのステートメントが1行以上でない限り、中括弧は使用しない。

    
    		for (p = buf; *p != '\0'; ++p)
    		    continue;
    
    		for (;;) 
    		    stmt;
    
    		for (;;) {
       			z = a + really + long + statement + that + needs +
       			  two + lines + gets + indented + four + spaces +
           		  on + the + second + and + subsequent + lines;
    		}
    
    		for (;;) {
    			if (cond)
           		stmt;
    		}	
    	    

    for ループの一部を空白のままにすることができる。

    
    		for (; cnt < 15; cnt++) {
    		    stmt1;
    		    stmt2;
    		}
    	    

    インデントは8文字のタブで、セカンドレベルのインデントは4スペースとする。 必ず、すべてのコードは80列に収まるようにすること。

    
    		while (cnt < 20)
    		    z = a + really + long + statement + that + needs +
    		     two + lines + gets + indented + four + spaces +
    		     on + the + second + and + subsequent + lines;
    	    

    行末にはホワイトスペースを追加しないこと。インデントを形成するためには、タブに続いてスペースを使用する。 タブよりも多くのスペースを使用しないこと。また、タブの前にスペースを使用しないこと。

    
    		if (test)
    		tmt;
    		else if (bar) {
    		stmt;
    		stmt;
    		} else
    		stmt;
    	    

    関数名の後にスペースを使用しないこと。カンマの後にはスペースを使用すること。 ‘(’または‘[’の後、‘]’または‘)’の前、またはそれに先立つ‘(’の後にはスペースを使用しないこと。

    
    		if ((error = function(a1, a2)))
    		exit(error);
    		

    単項演算子にはスペースは不要で、二項演算子にはスペースが必要である。

    
    		a = b->c[0] + ~d == (e || f) || g && h ? i : j >> 1;
    		k = !(l & FLAGS);
    	    

    exitは成功時には0、エラー時には非ゼロである必要がある。

    
    		/*
    		 * Avoid obvious comments such as
    		 * "Exit 0 on success."
    		 */
    	 	exit(0);
    	   

    関数宣言

    関数の前には、その関数を先行する行に型を置く必要がある。

    
    	static char *
    	function(int a1, int a2, float fl, int a4)
    	{
    	    

    関数内で変数を宣言する際は、サイズ順(大きい順から小さい順)、そしてアルファベット順にソートするように宣言すること。 複数の変数を1行に宣言しても問題はない。行がオーバーフローする場合は、タイプのキーワードを再利用すること。 変数の初期化でコードを難読化しないように注意すること。初期化は慎重に行い、初期化には関数呼び出しを使用しないこと。 しかし、安全性の観点から必ず宣言と初期化は同時に行うこと。未定義動作などを発生させる原因となる。

    変遷宣言

    
    	struct foo one = {}; /* Structure foo initialization */
    	struct foo *two = nullptr; /* Pointer to foo initialization */
    
    	double three = 0.0; /* Double initialization */
    	int *four = nullptr, five = 0; /* Pointer and integer initialization */
    	char *six = nullptr, seven = '\0', eight = '\0', nine = '\0', ten = '\0',
    	eleven = '\0', twelve = '\0'; /* Pointers and characters initialization */
    
    	/* Initialize four with the return value of myfunction() */
    	four = myfunction();
    
    	/* Add code to initialize one and two if necessary */
    	    

    しかし、以下の条件下では複数の変数を1行で宣言してはならない。 1番最初のconst*はポインタ自体が示すメモリ上のアドレスを変更できないことを示す。つまり、'a'が指す文字列へのポインタが指す先のデータは変更できない。 2番目のconstは、ポインタ変数'a'そのものが変更できないことを示している。つまり、'a'の指す値(アドレス)を変更できない。 [[deprecated]]この属性は、変数'a'が非推奨であることを示している。非推奨とは将来的なバージョンでサポートが終了する可能性があるということである。 このとき、一度にaとbを宣言してしまうと、bは型をunsigned constしか持っておらず、最初のconstはaの宣言にしか適応されない。

    
    	GOOD:
    	unsigned int const *const a = nullptr;
    	unsigned int const *const b = nullptr;
    
    	BAD:
    	unsigned  const* const a, b;
    	    

    このように、コンマ演算子(,)について、Cではときどき注意が必要である。また、クリーンなコードでは、ほとんど使わないことが好ましい。 例えば、A[i, j] において、カンマ演算子は i と j を順番に評価し、最後に j の値を返す。 これは通常の2次元配列の添字の意味ではなく、A 配列の j 番目の要素を参照することになる。

    
    	#include <stdlib.h>
    	#include <stdio.h>
    
    	int 
    	main(int argc, [[maybe_unused]]char *argv[argc + 1]) 
    	{
    		int i = 2;
    		int j = 3;
    		int A[6] = {
    			[0] = 0,
    			[1] = 1,
    			[2] = 2,
    			[3] = 3,
    			[4] = 4,
    			[5] = 5
    		};
    
    		/*
    		 * GOOD
    		 */
    		int result = A[j];
    			
    		/*
    		 * BAD
    		 */
    		// int result = A[i, j]; 
    	
    		printf("%d\n", result); 
    		
    		return EXIT_SUCCESS;
    	}
    
    		

    数値リテラル

    C23では、数値リテラルの可読性の向上させるため、アポストロフィを使って数字をグループ化することで、プログラマは大きな数字をより簡単に理解できる。 具体例として、数値が何バイトであるかを表すために、3桁ごとに区切ることが求められる。 これにより、数値のバイト数を直感的に理解しやすくなる。例えば、10'035.677'789は3桁ずつ区切ると10'035.677'789となり、これが3つの10進数桁で構成されていることがわかる。 同様に、16進数や2進数でも同様の考え方が適用される。

    
    	    /* Using digit separator for large decimal number */
     	   long large_number = 10'035'677'789; 
    
    	    /* Using digit separator for hexadecimal number */
     	   int hex_number = 0xABCD'1234; 
    
    	    /* Using digit separator for binary number */
     	   int binary_number = 0b1100'1010'0101'0010; 
    	    

    bool型 真偽値

    値が0というのはfalseを示す。逆に、0以外の任意の値はtrueを示す。 ==や!=演算子を使いそれぞれの真偽をテストできる。 以下のように書くことが好まれる。

    
    	    GOOD:
    	    if (i) {
    		// ...
    	    }
    
    	    BAD:
    	    if (i != 0) {
    		// ...
    	    }            
    	    

    また、冗長な比較は可読性が低く、コードを複雑化させる。 真偽値に依存する条件がある場合は、その真偽値を条件として直接使用する。 再び、次のようなものを書き直すことで冗長性を避けることができる。

    
    	    GOOD:
    	    bool b = ...;
    	    // ...
    	    if (b) {
    		// ...
    	    }
    
    	    BAD:
    	    bool b = ...;
    	    // ...
    	    if ((b != false) == true) {
    		// ...
    	    }
    	    

    関数内で関数を宣言しないこと。

    キャストと sizeof()の呼び出しの後にはスペースを入れないこと。

    "register" 指定子の使用は新しいコードでは非推奨である。 gcc などの最適化コンパイラは通常、コードのパフォーマンスを向上させるためにどの変数をレジスタに置くかをより良く選択できる。 ただし、"register" 指定子が必要な場合は、アセンブリコードを含む関数の場合のみである。

    テストに '!' を使用しないこと。ブール型の場合に使用すること。 if (*p == '\0') ではなく if (!*p)

    可能な限り、コードはコードチェッカー(たとえば、さまざまな静的アナライザー[gcc:--analyzer, clang:--alanyze]またはcc -Wall)を通過し、最小の警告を生成するようにするべきである。

    C23で追加された機能について

    基本的には積極的に使っていくべきである。

    1. 新しいキーワードの追加:

    2. 整数幅の制約の削除: 整数幅の制約や古い符号表現(「1の補数」と「符号-絶対値」)が削除された。

    3. static_assertの単一引数版の追加: static_assertの引数が1つだけのバージョンが追加された。

    4. 識別子リストを持つ関数定義のサポートの削除: 識別子リストを持つ関数定義のサポートが削除された。

    5. 空のパラメータリストを持つ関数宣言の扱いの変更: パラメータリストが空の関数宣言は、単一の void を含むパラメータリストと同じように扱われるようになった。

    6. POSIX との調整: POSIX との調整が行われ、strftime のための拡張月名形式や、いくつかの関数(gmtime_r, localtime_r, memccpy, strdup, strndup)の統合が行われた。

    7. IEC 60559 浮動小数点規格との調和: IEC 60559 浮動小数点規格との調和が行われ、バイナリ浮動小数点技術仕様 TS 18661-1、10進浮動小数点技術仕様 TS 18661-2、および数学関数技術仕様 TS 18661-4a が統合された。

    8. 定数式の認識の改善: constexpr 記憶クラス指定子とともに、オブジェクト定義のための constexpr 指定子が追加され、定数式として認識されるものが改善された。

    9. その他の様々な変更: その他にも、ビット単位の整数型 _BitInt(N)、nullptr 定数、nullptr_t 型、__VA_OPT__ 指定子、可変修飾型のサポート、#embed の導入、__has_include 機能の追加、#elifdef および #elifndef 条件付きインクルードプリプロセッサディレクティブの追加など、さまざまな変更が行われる。

    属性を含むアトリビュート機能を追加:

    deprecated: 将来使用しないことを示すエンティティをマークする。

    fallthrough: switch や label のフォールスルーが偶発的ではなく、意図的である場合を明示的にマークする。

    maybe_unused: 最終的に使用されないかもしれない実体をマークする。

    nodiscard: 使用された場合、その値がプログラムによって何らかの方法で処理されるべきエンティティをマークする。

    reproducible: 同じ入力が与えられると常に予測可能な出力を生成する関数タイプ(例えば、キャッシュされたデータ)をマークするため、しかしそのような関数の呼び出しの順序は依然として重要である。

    unsequenced: 常に予測可能な出力を生成し、他のデータへの依存性がない関数タイプ(およびその他の関連する注意事項)をマークするためのものです。

    noreturn: 関数が決して戻ってこないことを示す。

    
    	    int
    	    main(int argc, [[maybe_unused]]char *argv[argc + 1])
    	    {
    		return 0;
    	    }
    	    

    typeofとtypeof_unqual

    typeof:

    typeof演算子は、指定された式または型名の型を取得します。

    式が与えられた場合、その式の型が返されます。

    型名が与えられた場合、その型の情報が返されます。

    例えば、typeof(1 + 1)は式1 + 1の型を取得します。

    typeof_unqual:

    typeof_unqual演算子は、typeof演算子と同様に動作しますが、修飾を削除した型情報を返します。

    つまり、constやvolatileなどの修飾子が削除された型情報が返されます。

    例えば、typeof_unqual(const int)はintの型情報を返します。

    ビット精度の整数型 _BitInt の追加

    _BitIntは、ビット精度を指定するための型指定子である。指定されたビット数の整数型を表す。 以下に、_BitIntの使用例を示す。

    
    		    int 
    		    main(void) 
    		    {
    			_BitInt(8) byte; 
    			_BitInt(16) halfWord; 
    			_BitInt(32) word; 
    
    			byte = 255; 
    			halfWord = 65535; 
    			word = 4294967295; 
    
    			printf("Byte: %d\n", byte);
    			printf("Half Word: %d\n", halfWord);
    			printf("Word: %d\n", word);
    
    			return 0;
    		    }
    	    

    型推論

    型推論( Type inference; TI )とは、宣言がその初期化子として使用される式から型を推論することを可能にする機能である。 N3007提案では、auto i = 123L; の様に「型」の部分に auto と書くと初期化子(この場合は 123L)から変数(この場合は i)の型(この場合は long) を言語処理系が推論するという構文である(概ねC++と同じですが、C++には参照やラムダ式があるので若干事情が異なる)。 auto は今までもキーワードであったが、(静的変数に対して)「自動変数」を宣言することを表すもので、省略可能であった。

    
    			    int 
    			    main(void) 
    			    {
    				auto fname = "/etc/hosts";
    				auto file = fopen(fname, "r");
    
    				if (file == NULL) {
    				    perror(fname);
    				    return 1;
    				}
    
    				char line[256];
    				for (auto i = 1; fgets(line, sizeof(line), file); i++) {
    				    printf("%03d: %s", i, line);
    				}
    
    				fclose(file);
    				return 0;
    			    }