世界を動かす技術を、日本語で。

C言語におけるオブジェクト指向デザインパターンとカーネル開発

概要

  • OS開発の自由度 を活かし、独自の設計パターンを実践
  • C言語でのオブジェクト指向設計 による柔軟なカーネルサービス実装
  • vtable(関数ポインタ構造体) で動的な振る舞い変更を実現
  • 一貫したインターフェース により、異なるサービスやスケジューラの切替えが容易
  • デメリットと利点 を踏まえたカーネル開発の楽しさを強調

オリジナルOS開発におけるスケジューラ操作とvtable活用

  • OS開発(osdev)の魅力 は、他者との協業やリリース、セキュリティ維持の制約から解放される自由度
  • 孤独な開発環境 が、ユニークなプログラミングパターンの実験を可能に
  • Linuxカーネルの記事 (“Object-oriented design patterns in the kernel”)をきっかけに、C言語でもオブジェクト指向設計を導入
  • カプセル化・モジュール性・拡張性 をCで実現するため、関数ポインタを持つ構造体(vtable)を活用

vtableによるインターフェース設計

  • vtable(関数ポインタ構造体) でオブジェクトのインターフェースを定義
    • 例:struct device_ops に start/stop の関数ポインタを定義
  • 各デバイス構造体がvtableへの参照を保持
    • 例:struct deviceconst struct device_ops *ops を持つ
  • 異なるデバイスが同一APIで異なる挙動
    • 例:ネットワークデバイスとディスクデバイスで start/stop の実装が異なる

vtableの動的切替え

  • vtableの差し替え で、ランタイム中に振る舞いを変更可能
  • 呼び出し側コードに変更不要、柔軟な挙動進化を実現
  • 適切な同期処理 で安全な動的拡張

サービス管理への応用

  • OS内のサービス(ネットワーク管理、ウィンドウサーバ等) もvtableで統一的に管理
  • start/restart/stop の操作を持つサービスインターフェース
  • サービスごとの差異を吸収し、端末からの一貫操作を実現

スケジューラの実装

  • 複数のスケジューリング戦略(ラウンドロビン、優先度順等) をvtableで切替え可能
  • 必要な操作(yield, block, add, next) のみをインターフェース化
  • カーネル全体のコードは変更せず、戦略の差し替えが可能

Linuxの例:file_operations

  • Linuxカーネルの struct file_operations もvtableパターンの典型例
  • Unix/Plan 9の「全てはファイル」哲学 を支える統一インターフェース
    • ソケット、デバイス、テキストファイル全てが同一のread/write API

カーネルモジュールとの組み合わせ

  • カーネルモジュールとvtableの相性の良さ
    • vtable差し替えで、動的なドライバやフックの追加が可能
    • システム再起動や再コンパイル不要で拡張性向上

デメリットと利点

  • C言語でのvtable利用は冗長なシンタックス
    • 毎回 object->ops->start(object) のような呼び出しが必要
    • C++のような暗黙のthisがなく、関数シグネチャも冗長化
  • 明示的なthis渡しの利点
    • 関数の依存関係が明確になり、カーネルコードの結合度が可視化

まとめ

  • vtable活用により、カーネルコードの柔軟性と一貫性を両立
  • ランタイムでの挙動変更・新機能追加が容易
  • C言語の新たな活用法を発見し、OS開発の実験場としての楽しさを実感

参考文献

  • xine project でのvtableとプライベート変数の活用事例

Hackerたちの意見

記事では、LinuxカーネルがCで書かれているにもかかわらず、ポリモーフィズムを実現するために構造体内で関数ポインタを使ってオブジェクト指向の原則を取り入れていることが説明されています。この技術はオブジェクト指向プログラミングよりも前から存在しています。これは抽象データ型またはデータ抽象と呼ばれています。データ抽象とオブジェクト指向プログラミングの大きな違いは、抽象データ型では関数を未実装のままにできるのに対し、OOPでは関数を常に実装する必要があることです。オブジェクト指向プログラミングでオプションの関数を持つ最も理にかなった方法は、各オプション関数のために追加のクラスを作成し、基本クラスとともに多重継承で実装することだと思います。その後、オプション関数を使う前に、オブジェクトが追加クラスのインスタンスかどうかをランタイムでチェックする必要があります。抽象データ型では、関数ポインタが存在するかどうかを確認するために、単純なNULLチェックを行うだけで済みます。

大体同意しますし、Cでもこれらのパターンを使っていますが、クラシックなOOPのためのデフォルトまたはスタブ実装を基本に持つという一般的なアプローチを無視しています。また、よりモダンなOOPやコンセプトスタイルの言語では、インターフェースを使う選択肢もあります。インターフェース型にキャストすることで、実際に呼び出す必要があるAPIのサブセットだけを要求できます。Goはその良い例で、実際に関数ポインタのテーブルからランタイムでルックアップを行っています。

抽象データ型の概念は、コンパイラ設計の時代に実際に存在したアイデアです。「コンパイラ設計はオブジェクト指向プログラミングよりも前から存在している」と言ってもいいでしょう。リードで説明されている技術は、オブジェクト指向プログラミングの構造を実装するために使われています。実際、コンパイラ設計の多くの機能がその背後にあります。ソースとして、当時MetroWerksでCを使ってこのパターンや他のものを使ってMacOSのウィンドウフレームワークを書きました。

SmalltalkやObjective-Cでは、オブジェクトインスタンスがメッセージに応答するかどうかをランタイムでチェックします。これが元々のOOPのやり方です。OOPが過度にクラス中心のC++やJavaのデザインパターンによって腐敗したのは悲しいことです。

この技術はオブジェクト指向プログラミングよりも前から存在しています。OOPは、前から存在していたパターンやパラダイムの形式化だと言った方がいいでしょう。

コンポジットパターンが使えるなら、継承は必要ないよ。 class DefaultTask { } class SpecialTask { } class UsedItem { UsedItem() { _task = new SpecialTask() } void DoIt() { _task.DoIt() } } PythonってOOP言語なの?self / this / オブジェクトポインタは、Cスタイルのオブジェクト指向やデータ抽象を使うのと同じように渡さなきゃいけないよね。

JavaやC#のようなほとんどのOOP言語でCでやったことをそのままできるよ。だって、今はラムダがあるからね。ラムダはただの関数ポインタなんだ。インスタンス変数(または静的変数)に割り当てることもできるよ。(ごめん、Javaが追いつくのに10年以上かかったし、Sun Microsystemsは昔ラムダをJavaに追加しようとしたMicrosoftを訴えたんだ。それで、匿名内部クラスが十分に良い代替品だって主張するホワイトペーパーまで書いたんだよ - 笑わないでね)

オブジェクトを毎回明示的に渡さなければならないのは、特にC++と比べると不格好に感じます。個人的には暗黙のthisは好きじゃないです。これはクラスメソッドではなく、thisインスタンスを渡していることになりますからね。また、明示的なthisは、変数がインスタンス変数なのかグローバル変数なのか、どこから来たのかを知らないという問題を解消します。

また、明示的なthisは、変数がインスタンス変数なのかグローバル変数なのか、どこから来たのかを知らないという問題を解消します。人々は通常、メンバー変数のために何らかの命名規則を使います。例えば、mFoo、m_Foo、m_foo、foo_などですから、これは問題ではありません。foo_の方がthis->fooよりもずっと簡潔だと思います。また、C++では本当に必要なら明示的なthisを使うこともできますよ。

...そしてC++はC++23で明示的なthisパラメータ(thisを推測する)を追加しました。

著者が言ってるのはこれだと思う:object->ops->start(object) ここでは、明示的であるだけでなく、オブジェクトを2回指定する必要があるんだよね(1回目はVtableを解決するため、2回目はそのオブジェクトをステートレスなCメソッドに渡すため)。

「this」はC++で予約語だから、グローバル変数になる心配はないよ。ただ、Cの抽象データ型(ADT)みたいに、明示的にthisポインタを渡すのが好きなんだ。thisポインタが必要ない関数は、開発者が関数をstaticにするのを忘れたり、全ての関数アクセスを::演算子を使うように書き換えたくない時に、うっかり渡されることがないからね。

マクロを使って賢くやることもできるよ。

同意だな。C++(あとJavaも)でのOOP構文の一番のデザインミスは、インスタンスメンバーを参照する時にthisを必須にしなかったことだと思う。

暗黙のthisって、なんか魔法みたいに感じるんだよね。魔法!「これどうやってやるの?」って聞くと、「ほら、魔法だから」って。勝手に起こるんだよ。何かうまくいかなかった?それも魔法。40年経って、魔法はもう嫌だ。

ちょっと違うと思うな。特に暗黙のthisは、明示的に書かなくて済むだけじゃなくて、実際のメソッドがあれば、毎回関数に構造体のサフィックスを付けなくてもいいから。mystruct_dosmth(s); mystruct_dosmthelse(s); じゃなくて、s->dosmth(); s->dosmthelse(); って感じで。

Tmuxについてのトーク[0]で、このCのパターンについて学んだんだ。自分の理解のためにこのコンセプトについても書いたよ[1]。tmuxのコードを通してパターンのインスタンスを追ってみたんだ。 [0] https://raw.githubusercontent.com/tmux/tmux/1536b7e206e51488... [1] https://blog.drnll.com/tmux-obj-oriented-commands

これが使っているのはインターフェース(つまり、vtables、関数ポインタの記録)で、完全なオブジェクト指向ではないことに注意してね。他のOOの特徴、例えばクラスや継承は、もっといろいろなものを抱えていて、しばしばその痛みを考えると価値がないことが多い。

フィールドの継承はCでは意外と自然で、structを最初のメンバーにキャストできるんだよね。

継承って、vtablesの合成以外の何だと思う?クラスって、vtableとスコープ付き変数の合成以外の何だと思う?

vtableには「this」ポインタを受け取る関数へのポインタが含まれてるんだ。著者はstruct file_operationsをvtableの例として挙げてるけど、struct file_operationsには「this」ポインタを受け取らない関数へのポインタが含まれてる。それはvtableですらないよ。

数年前、PeterpaulがCの上に軽量なオブジェクト指向システムを開発したんだけど、ほんとに使いやすかったよ[0]。オブジェクトを明示的に渡す必要もないし。ドキュメントはあまり良くないけど、フルテストスイートがあるんだ(例えば、[1][2])。 [0] https://github.com/peterpaul/co2 [1] https://github.com/peterpaul/co2/blob/master/carbon/test/pas... [2] https://github.com/peterpaul/co2/blob/master/carbon/test/pas...

カーボンのシンタックスシュガーなしでどうなるか気になる人は、ここを見てみて:[0]。私が見る限り、パラメトリックポリモーフィズムのサポートはないみたい。0. https://github.com/peterpaul/co2/tree/master/examples/my-obj...

Valaもこのニッチに入り込もうとしてる気がする。

いつも不思議に思うんだけど、なんで似たようなものが新しい(いくつかの)Cバージョンに入らなかったんだろう?明らかに需要はかなりあるし、同じ(似たような)パターンを再実装してる人がたくさんいるよね。

おそらくHigh C Compilerに入ってるんじゃないかな。

シンタックスシュガーを作る時は、いくつかの使い方を許可し、他の使い方は不可能にするか、古い方法に戻る必要があるようにしないといけない。詳しくはここを見てね:https://news.ycombinator.com/item?id=45040662。また、Cのポイントの一つは、動的な複雑さを隠さないことだよ。動的ディスパッチがある時はいつでもそれが見える。これらの概念に対して形式主義を導入する言語はたくさんあるけど、正直言って、ほとんどの現代の命令型言語はそうだね。Cのユニークなセールスポイントは、その複雑さが見えること。だから、本当に必要な時だけ使うようになるんだ。文法もそんなに複雑じゃないしね。

このアプローチのもう一つの面白い点は、オブジェクトの初期化に渡す引数を構造体へのポインタにできることだよ。そうすれば、コード全体でオブジェクトを初期化する呼び出しを変えずに、後からオブジェクトに機能を追加できるんだ。

だいたいvtable関数の周りにインラインラッパーを作って、thing->vtable->foo(thing, ...)foo(thing, ...)にしてるよ。

大学の時にいくつかの小さなプロジェクトでこれやったことあるよ。CにOOPっぽいものを持ち込むのは楽しいけど、注意しないとすぐにトラブルに巻き込まれるからね。