SNSへはこちら

GCCによる関数の属性(__attribute__)について調べてみた

C言語および C++ における属性の話です。
属性とは簡単に言うとコンパイラに「これはこれこれこういう機能を持った特別な関数ですよ〜」って教えるものになります。GCC の拡張ですので ANSI じゃなきゃダメな方は回れ右。

記述法については以下の通り。

__attribute__((hoge)) void f(void); // hoge という属性を付加

括弧に関してなのですが、二重に書いてください。一重だとエラーが出ました。
この hoge という属性は勝手に決めて良いものではなく、実行先のアーキテクチャ・プラットフォームで決まっているものです。
どのプラットフォームでどの属性が使えるかですが、gcc.gnu.orgにその解説があったのでリンクを張っておきます。

このリンクを見といてください、と言ったらそれまでなのですが、この記事では独断と偏見によってこれ面白いじゃんと思ったものを掲載します。

これまでに出てきた例

その前に、これまでに __attribute__ を用いたソースコードが出てきました。それは MSP430 マイコンを弄った時の話です。かつての自分は割り込み関数の登録にハマっていたみたいです。このときに属性を使いましたね。この記事です。
具体的にコードを抜き出すと、こんな感じにサンプルコードを書いていました。

__attribute__((interrupt(TIMER0_A0_VECTOR))) void TimerA0_Vect(void) {
  hoge;
  hage;
}

この interrupt(TIMER0_A0_VECTOR) と言うものが、これは割り込みハンドラですよとコンパイラに伝えている部分になります。こうやって割り込み属性を付加するのに用いることがあります。
確か RX とかは使っていた気がするなあ。

weak

直訳で「弱い」という意味ですが、その名の通り弱い関数定義を行っておくと言うものになります。マイコンでよくある割り込みハンドラも、中身が空の状態で weak 定義されていることが多いです。

では具体的にどうなるかですが、以下を考えてみましょう。2つのファイルに分かれているプログラム(main.c と sub.c)について考えます。

// sub.c
#include <stdio.h>
__attribute__((weak)) void f(void) {
    printf("Hi\n");
}

// sub.h
void f(void);

// main.c
#include "sub.h"
int main(void) {
    f();
    return 0;
}

このプログラムを実行すると、当然出力装置には Hi と挨拶が表示されます。ここで以下を考えてみましょう。

// sub.c
#include <stdio.h>
__attribute__((weak)) void f(void) {
    printf("Hi\n");
}

// sub.h
void f(void);

// main.c
#include "sub.h"

void f(void) {
    printf("Hello!\n");
}

int main(void) {
    f();
    return 0;
}

通常だと同一名関数の多重定義でエラーとなりますが、上の sub.c では f() に weak 属性を付けているので、weak の無い同一名関数が登場した瞬間に、Hi と出力する元の f() は無効化され、リンクされません。ということで weak ではない方の関数が実行され、Hello! と表示されます。これは上に張ったリンク先にもある通り、ライブラリ等で用いると便利だということです。すなわちライブラリ中でユーザーが何も指定しない場合はデフォルトの動作をし、ユーザーが指定してきた場合はそちらを優先する、といったことが可能になるので便利だというわけです。

関数エイリアス

ある関数を別名で呼ぶといったものです。まず実体を定義し、次に別名で関数プロトタイプ宣言を記述します。そこに「これは、この関数のエイリアスのエイリアスですよ」と書くのです。注意点として、weak 属性を付さねばならぬということです。

int f(int x) {
    return 2 * x;
}

__attribute((weak, alias("f"))) int g(int);

こうすると、f(2) -> 4 ですし、g(5) -> 10 となります。残念ながら、Mac では関数のエイリアスが無効なようで、エラーで確認できませんでした。

コンストラクタ・デストラクタ

C++ でしょうか。いいえ、どちらも。ということで、クラスのそれとは異なるコンストラクタ・デストラクタの属性です。まずコンストラクタなのですが、main 関数を実行する前に実行する関数となります。こちらはそのまんま文字通り。

__attribute__((constructor)) void begin(void) {
    printf("OK, start the program\n");
}

こう書くだけで、main 関数の実行の直前に begin() が呼び出されます。

一方デストラクタですが、こちらは main 関数実行後はもちろん、exit 等で異常終了させた直後にも実行されます

__attribute__((constructor)) void end(void) {
    printf("Bye!\n");
}

フォーマット

こちらは printf や scanf 等の引数を用いる関数を自作した時に便利な属性です。これを指定するとコンパイル時に "%d" 等のフォーマット文字列と実際に指定された変数の型を比較して Warning を出す等が出来ます。実際の属性指定はこんな感じでやります。

__attribute__((format(printf,1,2)))

printf はどの関数と同じ引数か、ということを書きます。対応しているのは printf, scanf, strftime, strfmon となっています。続いて2つの数字が続くのですが、1つ目はフォーマット文字列がその関数の何番目のものか、そして2つ目が最初にチェックする変数が引数の何番目に来ているかを表します。

ここで C++ のクラス内でこれを用いる時の注意です。Stack Overflow の質問によると、C++ のクラス内関数(static ではないもの)は実際の所第1引数が自身へのポインタ(this ポインタ)となっているようです。ですので例えば宣言では1つ目にフォーマット文字列、2番目以降に変数が来る printf 的な関数の場合は __attribute__((format(printf,2,3))) と1つズレた値にせよ、ということらしいです。