最近、Emacsのinit.elを編集していました。すると、global-set-keyなどの考え方が、以前にWordPressプラグインをいくつか作ったときのadd_filterやadd_actionなどの構造に似ていることに気が付きました。
このような設計は「フック設計」と呼ばれ、拡張性を重視するソフトウェアには広く見られます。
たとえば、Google Chromeの拡張機能やVS Codeのプラグインなど。
今回は、「フック設計」について整理していきましょう。
1. フック設計とは何か
フック設計の本質は、とてもシンプルです。
ソフトウェアのコア部分が「ここで外部のコードを呼び出せるようにしよう」という地点を用意し、そこに好きな機能を引っ掛けられるようにする、という仕組みです。
言葉で説明するより、最も原始的な形を見たほうが早いと思います1。
たとえば、このようなコードでは、メイン処理のあとにmy_pluginの処理が実行されます。
void my_plugin() {
printf("プラグインが実行されました\n");
}
int main() {
add_hook(my_plugin); // 登録
printf("メイン処理\n");
run_hooks(); // 実行
}
Code language: JavaScript (javascript)
出力はこうなります。
メイン処理
プラグインが実行されました
メイン処理が終わったあと、登録しておいた関数が呼ばれました。
このadd_hook, run_hooksがフック設計の原始的なかたちです。
// 1. 関数ポインタの型定義
typedef void (*hook_function)(void);
// 2. フック登録用の配列
hook_function hooks[100];
int hook_count = 0;
// 3. フック登録
void add_hook(hook_function func) {
hooks[hook_count++] = func;
}
// 4. フック実行
void run_hooks(void) {
for(int i = 0; i < hook_count; i++) {
hooks[i](); // 登録された関数を順番に呼ぶ
}
}
Code language: JavaScript (javascript)
関数のアドレスを配列に保存しておき、必要なタイミングで順番に呼び出す。
フック設計の根底にあるのは、この単純な仕組みです。
1.1. Emacsでのフックの例
Emacsの設定ファイル(init.el)では、このような設定がたくさんあります。
add-hook関数には、フックできるタイミングも追加され、特定の操作や処理のタイミングで、自動的に実行する処理を付け加えるようになっています。
;; 1. 最もシンプルな形:関数を直接フックに追加
(add-hook 'text-mode-hook 'turn-on-auto-fill)
;; 2. 無名関数(lambda)を使う形
;; ファイル保存時に実行されるフック
(add-hook 'before-save-hook
(lambda ()
(delete-trailing-whitespace))) ; 行末の空白を削除
;; バッファ切り替え時
(add-hook 'buffer-list-update-hook
(lambda ()
(message "Buffer changed to: %s" (buffer-name))))
;; プログラミングモード全般で実行
(add-hook 'prog-mode-hook
(lambda ()
(hl-line-mode 1) ; 現在行をハイライト
(display-line-numbers-mode 1))) ; 行番号表示
Code language: PHP (php)
1.2. 実際のソフトウェアでの進化(WordPress)
基本形はシンプルですが、実用的なシステムではいくつかの要素が追加されています。WordPressを例に見てみましょう2。
// 名前付きフック(複数のフックポイントを管理)
$hooks = [
'the_content' => [],
'the_title' => [],
];
// 優先度(実行順序の制御)
function add_filter($hook_name, $function, $priority = 10) {
global $hooks;
$hooks[$hook_name][$priority][] = $function;
}
// データの受け渡し
function apply_filters($hook_name, $value) {
global $hooks;
foreach($hooks[$hook_name] as $priority => $functions) {
foreach($functions as $func) {
$value = $func($value); // 前の結果を次に渡す
}
}
return $value;
}
Code language: PHP (php)
基本形に比べて、3つの要素が追加されています。
- 名前空間によって、複数のフックポイントを識別できるようになりました。
「記事のタイトル」と「記事の本文」で別々のフックを用意し、それぞれに異なる処理を登録できます。 - 優先度で実行順序を制御できます。
「まずスパムチェック、次に内容の整形」のように、処理の順序が重要な場合に使います。 - 引数の受け渡しで、データを変換するパイプラインを作れます。
前のフックの結果を次のフックに渡すことで、複数の処理を連鎖させられます。
でも根本は変わっていません。
「関数のリストを管理して順番に呼ぶ」という原理は同じです。
2. なぜこの設計が広まったのか
フック設計は、多くのソフトウェアで採用されています。
その背景には、いくつかの歴史的な転換点がありました。
2.1. Emacsからの流れ(1976年)
最も古い成功例の一つは、1976年のEMACSだと考えられます3。
MIT AI LabでRichard Stallmanらが作ったこのエディタは、TECOエディタのマクロとして実装されていました。
1978年になると、Bernie GreenbergがMultics EmacsをMacLispで実装しました4。
これがLispを拡張言語として使った最初のEmacsで、フック機構の基盤を確立しました。
Emacsの設計思想は、「エディタ自体を拡張可能にする」というものでした。
ユーザーが自分の作業に合わせてエディタをカスタマイズできる。
この考え方が、後のソフトウェアに大きな影響を与えました。
2.2. Eclipse:エンタープライズへの展開(2001年)
2001年にリリースされたEclipseは、プラグインアーキテクチャを中核に据えたIDEでした5。
後にOSGiフレームワークを採用し、プラグインアーキテクチャをソフトウェアエコシステムの技術基盤として確立しました。
Eclipseの登場によって、エンタープライズ開発者がこの設計パターンを「標準的な選択肢」と認識するようになりました。
企業向けのツールでも、拡張可能な設計が当たり前になっていったのです。
2.3. Firefoxと一般ユーザーへの普及(2004年)
2004年11月にリリースされたFirefox 1.0は、XULベース(XML User Interface Language)の強力な拡張機能を備えていました6。
開発者はFirefoxを構築するのと同じ技術(XUL、XPCOM)を使って拡張機能を作成でき、これが非常にシンプルで強力でした7。
Firefoxの影響は大きかったと思います。
一般ユーザーが「拡張機能でソフトウェアをカスタマイズできる」ことを体験し、数千のアドオンエコシステムが形成されました。
技術者でなくても、プラグインの恩恵を受けられるようになったのです。
2.4. VS Code:現代的な実装(2015年)
2015年にリリースされたVS Codeは、拡張性を念頭に置いて構築されました8。
UIから編集体験まで、ほぼすべての部分がExtension API経由でカスタマイズ可能です。
VS Codeの設計は洗練されていて、軽量で高速なエディタに強力な拡張エコシステムを組み合わせることに成功しました。
現代の開発者にとって、拡張可能なエディタが標準になった背景には、VS Codeの影響があると捉えています。
3. フック設計の重要なコンポーネント
フック設計を実装する際、いくつかの重要なコンポーネントが必要になります。
それぞれ見ていきましょう。
3.1. フックポイントの設計
フックポイントは、コア機能のどこで拡張を許可するかを定義する地点です。
class Application:
hooks = {
'before_start': [],
'after_start': [],
'before_request': [],
'after_request': [],
'on_error': [],
'before_shutdown': []
}
フックポイントの設計で考えるべきは、粒度です。
細かすぎると複雑になり、粗すぎると柔軟性が不足します。
また、命名も大切です。user_loginよりafter_authentication_successのほうが、何が起きた後のフックなのかが明確に伝わります。
3.2. レジストリによる管理
登録されたフックを管理するデータ構造が必要です。
class HookRegistry:
def __init__(self):
self._hooks = defaultdict(list)
def register(self, hook_name, callback, priority=10):
self._hooks[hook_name].append({
'callback': callback,
'priority': priority
})
# 優先度でソート
self._hooks[hook_name].sort(key=lambda x: x['priority'])
レジストリは単なる辞書やリストでも機能しますが、優先度管理や重複検出などの機能を追加すると、より使いやすくなります。
3.3. 実行エンジン
登録されたフックを呼び出すメカニズムです。
def execute_hooks(hook_name, *args, **kwargs):
for hook in registry._hooks[hook_name]:
try:
hook['callback'](*args, **kwargs)
except Exception as e:
# エラーハンドリング戦略
log_error(e)
# 続行 or 中断?
Code language: PHP (php)
ここで重要な判断が必要になります9。
1つのフックが失敗したとき、残りも実行するのか、全体を停止するのか。
どちらが正しいかは、システムの性質によります。
3.4. コンテキストと引数の受け渡し
フックに何を渡すかも、設計の重要な部分です。
# Action型(戻り値不要)
def on_user_login(user, timestamp, ip_address):
send_notification(user.email, "New login detected")
# Filter型(データ変換)
def modify_content(content):
content = add_watermark(content)
return content
# 実行
content = "original"
for hook in hooks['content_filter']:
content = hook(content) # 前の結果を次に渡す
Code language: PHP (php)
Action型とFilter型という2つのパターンがあります。
Action型は副作用のみで戻り値を使わず、
Filter型はデータを変換して次のフックに渡します。
3.5. ディスカバリーメカニズム
プラグインをどう見つけるかも考える必要があります。
# ファイルシステムスキャン
for plugin_dir in scan_directory('/plugins'):
manifest = load_json(plugin_dir / 'plugin.json')
register_plugin(manifest)
Code language: PHP (php)
ディレクトリをスキャンする方法や、設定ファイルで明示的に指定する方法、パッケージのエントリーポイントを使う方法など、いくつかのアプローチがあります。
3.6. メタデータとマニフェスト
プラグインの説明情報も必要です。
{
"name": "spam-filter",
"version": "1.0.0",
"hooks": {
"email_received": {
"function": "check_spam",
"priority": 5
}
},
"dependencies": ["email-parser"],
"permissions": ["read_email", "modify_headers"]
}
Code language: JSON / JSON with Comments (json)
名前、バージョン、依存関係、必要な権限などを記述します。
これにより、プラグイン同士の互換性をチェックできます。
4. スクリプト言語とコンパイル言語での違い
フック設計の実装は、言語によって異なります。
4.1. スクリプト言語の場合
WordPressやEmacsのようなスクリプト言語ベースのシステムでは、インタープリタが必要です。
// WordPress
include 'plugin.php'; // この時点でPHPインタープリタが解釈
Code language: PHP (php)
プラグインコードは実行時に読み込まれ、その場で解釈されます。
WordPressの場合、Apache/Nginx起動後にPHPエンジン(Zend Engine)が起動し、プラグインディレクトリをスキャンして各プラグインの.phpファイルを解釈します10。
4.2. コンパイル言語の場合
C/C++のような言語では、事前にコンパイルされた共有ライブラリとして読み込みます。
// Linuxの例
void* handle = dlopen("plugin.so", RTLD_LAZY); // バイナリを読み込み
hook_function func = dlsym(handle, "my_plugin"); // 関数アドレス取得
add_hook(func); // 登録
Code language: JavaScript (javascript)
スクリプトエンジンは不要ですが、プラグインと本体のバイナリ互換性を保つ必要があります。
コンパイラのバージョンやリンクするライブラリが異なると、動作しなくなる可能性があります。
5. フック設計の利点と課題
フック設計には明確な利点があります。
- 疎結合が実現できます。
コア部分と拡張部分を分離できるため、それぞれを独立して開発・テストできます。 - 安全性も高まります。
コアコードを変更せずに機能を追加できるため、既存の動作を壊すリスクが減ります。 - 保守性も向上します。
プラグイン単位で更新・削除ができるため、特定の機能だけを切り離すことが容易です。 - コミュニティ主導の開発も促進されます。
サードパーティが自由に拡張できるため、エコシステムが形成されやすくなります。
一方で課題もあります。
- フックポイントを一度公開すると、後方互換性を維持する必要があります。
APIを変更すると既存のプラグインが動かなくなるため、慎重な設計が求められます。 - パフォーマンスにも影響があります。
多数のフックが登録されると、それぞれを順番に実行するオーバーヘッドが発生します。 - デバッグの難しさも増します。
複数のプラグインが絡み合うと、どこで問題が起きているのか特定しにくくなります。
5.1. セキュリティとサンドボックス(XUL/XPCOM)
フック設計では、プラグインの権限制御も重要です。
Firefoxの旧拡張機能(XUL/XPCOM)は強力でしたが、セキュリティの問題がありました11。
2017年にFirefox 57で廃止された理由の一つは、拡張機能がブラウザの内部に深くアクセスできすぎたことです。
悪意のあるプラグインは、キー入力をすべて記録したり、パスワードマネージャーを改変してパスワードを外部に送信したりできました12。
実際に、そのような挙動をする拡張機能が存在していました。
現代のシステムでは、権限の明示的な宣言が一般的です。
class SandboxedPlugin:
allowed_operations = ['read_file', 'write_log']
def execute(self, operation, *args):
if operation not in self.allowed_operations:
raise PermissionError(f"{operation} not allowed")
return getattr(self, operation)(*args)
プラグインができることを制限し、必要な権限だけを与えることで、セキュリティリスクを減らせます。
5.2. 優れたフック設計の特徴
ここまで見てきた内容を踏まえると、優れたフック設計にはいくつかの共通点があると捉えています。
- 最小限のフックポイントで最大限の拡張性を提供すること。
フックポイントが多すぎると複雑になり、少なすぎると柔軟性が不足します。 - 明確な契約があること。
引数、戻り値、副作用を文書化し、プラグイン開発者が何を期待できるかを明確にします。 - 予測可能な実行ができること。
フックの実行順序が明確で、開発者が挙動を予測できます。 - 堅牢なエラー処理があること。
1つのプラグインが失敗しても、全体が停止しないような設計が望ましいです。 - 後方互換性を考慮すること。
API変更時の移行パスを用意し、既存のプラグインが突然動かなくなることを避けます。
これらが揃って初めて、実用的なプラグインシステムが構築できます。
6. まとめ
フック設計は、「関数ポインタの配列」という単純な仕組みから始まりました。
それが時代とともに進化し、現代のソフトウェアエコシステムを支える重要な設計パターンになりました。
EmacsやWordPressを触っているとき、ChromeやVS Codeの拡張機能を使っているとき、その背後にはこの仕組みが働いています。
一見異なるソフトウェアでも、根底にある考え方は共通しています。
拡張性が重要になるシステムでは、この設計パターンを知っておくことが役立つと思います。
- この基本的な実装パターンは、C言語の関数ポインタを使った古典的な設計手法です。関数ポインタ配列による拡張メカニズムは、1970年代のUNIXシステムプログラミングから使われてきました。 – Notes on the Eclipse Plug-in Architecture
- WordPressのフックシステムは、イベント駆動型アーキテクチャの実装例として広く研究されています。add_filterとadd_actionの2つの主要な関数により、プラグイン開発者はWordPressのコア機能を変更せずに拡張できます。 – Plug-In Architecture. and the story of the data pipeline…
- 1976年にRichard Stallman、Guy Steele、Dave MoonがMIT AI LabでTECOエディタ用のマクロとして最初のEMACS(Editor MACroS)を作成しました。これがマクロベースの拡張性を実現した最初期の例です。 – Emacs – Wikipedia
- Bernie GreenbergによるMultics Emacs(1978年)は、Lispを拡張言語として使用した最初のEmacsバージョンであり、現代のEmacs Lispの直接的な祖先となりました。これによりユーザーが実行時にエディタの動作をプログラムで変更できるようになりました。 – Multics Emacs History/Design/Implementation
- Eclipseは後にOSGi(Open Services Gateway initiative)フレームワークを採用し、動的なモジュールシステムを実現しました。OSGiは、Javaにおけるモジュール性とパッケージの明示的なエクスポート/インポートを提供し、Eclipseのプラグインエコシステムの基盤となりました。 – The Architecture of Open Source Applications – Eclipse
- FirefoxのXUL(XML User Interface Language)ベースの拡張機能は、ブラウザのUIを含むほぼすべての部分を変更できる強力なシステムでしたが、セキュリティとパフォーマンスの問題から2017年のFirefox 57で廃止され、WebExtensions APIに置き換えられました。 – Why Did Mozilla Remove XUL Add-ons?
- XUL/XPCOMベースの拡張機能は「Promiscuous Extension Mechanism(無制限な拡張機構)」と呼ばれ、拡張機能がブラウザの内部APIに完全にアクセスできました。これにより強力なカスタマイズが可能でしたが、同時にセキュリティリスクと保守性の問題も生み出しました。 – A Brief History of Browser Extensibility
- VS Codeの拡張機能は、Extension APIを通じてエディタのほぼすべての部分にアクセスできます。拡張機能は遅延ロード(lazy-load)され、Activation Eventsによってトリガーされることで、パフォーマンスへの影響を最小限に抑えています。 – Discovering Plug in Play architecture – Exploring Successful Plug-and-Play Implementations
- プラグインアーキテクチャでは、エラーハンドリング戦略が重要です。1つのプラグインの失敗が全体に影響しないよう、各プラグインを独立したエラー境界で実行することが一般的です。これは「fail-fast vs fail-safe」のトレードオフを考慮した設計判断です。 – Plugin systems – when & why?
- WordPressのプラグイン読み込みプロセスでは、wp-config.phpの読み込み後、wp-content/pluginsディレクトリ内の各プラグインフォルダがスキャンされ、plugin.phpまたはメインのPHPファイルが順次読み込まれます。この時点でadd_actionやadd_filterの呼び出しが実行され、フックが登録されます。
- Mozillaは2017年のFirefox 57(コードネーム: Quantum)で、マルチプロセスアーキテクチャ(Electrolysis/e10s)との互換性問題とセキュリティ上の理由から、XUL/XPCOMベースの拡張機能のサポートを終了しました。新しいWebExtensions APIは、Chrome拡張機能と互換性のある制限されたAPIセットを提供します。 – Firefox Quantum Commits to Cross-Browser Extension Architecture
- 2017年以前のFirefoxでは、市場をリードする製品のインストーラーが、ユーザーに無断で見えない拡張機能をインストールし、ブラウザの重要機能を乗っ取ってプライバシーを侵害する事例が実際に確認されていました。このような問題が、より制限的なWebExtensions APIへの移行を促した要因の一つです。 – Why Did Mozilla Remove XUL Add-ons?