C言語プログラミングの基礎・Callback関数(コールバック関数)の実装をご紹介します。
Callback関数を使用することで、非同期処理の応答を取得することができ、また依存関係の低減に貢献できます。
Callback関数とは
Callback(コールバック)関数とは呼び直してほしい関数のことです。
言葉の由来は電話のかけ直と同じことから来ているそうです。
電話で例えると、電話をすると相手側の電話機に電話番号が記録されます。相手側は後でその電話番号をたどってかけ直すことができます。
プログラムで言うと、相手側となる他スレッドに対してコールバック関数を登録することで他スレッドから登録されたコールバック関数を呼ぶことができます。
用途・どんな時に使えるの?
主に以下の目的で使用します。
- 非同期要求に対して応答する
- 下位モジュールが上位モジュールへ通知する
用途はC++のオブザーバパターンと同じです。コールバック関数はC++でいう基底クラスの関数の使用やオーバーライドの役割をもっています。オブザーバパターンをコールバック関数で代替できます。
配置図
C言語にはクラス概念はありませんが、説明のために配置図を作りました。
図ではファイル名を示しています。各ファイルの関係性としてはcontents_frameworkが上位モジュール、contentsが下位モジュールとなります。
contents_frameworkがコールバック関数であるupdate関数をcontentsへ登録します。contentsはhandlerであるcall_back_wrapper関数をコールすることで上位モジュールのupdate関数をコールすることができます。
※ExCallbackはただのmain関数であり、使用者です。
コールバック関数を使う理由
非同期処理の応答を受けるためとモジュール間の依存関係(相互参照を防ぐ)を減らすためです。
この記事では後者の依存関係を減らすことに着目します。
モジュール間の依存関係を減らす
コールバック関数を使わなくてもcontentsがcontents_frameworkをコールすることはできます。何故ややこしいコールバック関数を使うのかというと、冒頭で述べた通り依存関係を減らすためです。
コールバック関数を使用しない場合はcontents_frameworkとcontentsは相互に依存関係を持つこととなり、コード変更に弱くなります。具体的に言うとcontents_frameworkのupdate関数名が変更になればcontents側も修正が必要になり、修正の規模が大きくなってしまいます。
コールバック関数を使用する場合はcontentsはcontents_frameworkで定義している関数を意識する必要がないため、update関数名が変更になってもcontents_frameworkのみの修正で対応することができコード変更に強いです。コールバック関数を使うことでコード変更の規模を小さくできます。
実装の解説
C言語で実装したサンプルコードを解説します(ファイル名はcppですが中身はC言語です)。
このサンプルコードはコールバック関数(update関数)を下位モジュールへ設定し、下位モジュールからコールバック関数をコールします。
まずtypedefを使用して戻り値void, 引数charポインタの関数ポインタをp_funcに置き換えます。contents_runの引数にはupdate関数のポインタが指定されているので、一度内部の関数ポインタに登録します。
call_back_wrapper関数で関数ポインタをコールすることで、上位モジュールであるcontents_frameworkのupdate関数がコールされます。
■ contents_framework.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include "contents_framework.h" #include "contents.h" #include "stdafx.h" #include "logout.h" /* run method. */ void run() { // コールバック関数(update)を設定. contents_run(update); return; } /* コールバック関数. */ void update(char* str) { LOG_OUTPUT2("%s", str); return; } |
■ contents_framework.h
1 2 3 4 5 6 7 8 9 |
#pragma once #ifndef _CONTENTES_FRAMEWORK_H_ #define _CONTENTES_FRAMEWORK_H_ void run(); void update( char* str ); #endif /* _CONTENTES_FRAMEWORK_H_ */ |
■ contents.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include "contents.h" #include "logout.h" /* callback wrapper method. */ void call_back_wrapper(p_func ptr, char* s) { ptr(s); } /* contesnts_run method. */ void contents_run( p_func func_ptr ) { p_func contents_ptr; // 関数ポインタにコールバック対象の関数を設定. contents_ptr = func_ptr; // コールバック関数をコールする. call_back_wrapper(contents_ptr, "Update by contesnts." ); return; } |
■ contents.h
1 2 3 4 5 6 7 8 9 10 |
#pragma once #ifndef _CONTENTES_H_ #define _CONTENTES_H_ typedef void (*p_func)(char*); void contents_run( p_func func_ptr ); #endif /* _CONTENTES_H_ */ |
■ ExCallback.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// ExCallback.cpp : コンソール アプリケーションのエントリ ポイントを定義します。 // #include "stdafx.h" #include "contents_framework.h" #include "logout.h" int main() { LOG_OUTPUT1("Start."); // テスト開始. run(); LOG_OUTPUT1("Terminate."); return 0; } |
サンプルコード
Visual C++ 2017で作成したサンプルコードをGitHubで公開しています。
GitHub – ExCallback
実行結果は以下の通りです。コールバック関数であるupdate関数がコールされていることが確認できます。
1 2 3 4 5 |
[text] [ 10][ main] Start. [ 18][ update] Update by contesnts. [ 15][ main] Terminate. [/text] |