36 ポイント 投稿者 GN⁺ 2025-08-29 | 1件のコメント | WhatsAppで共有
  • オブジェクト指向デザインパターンは、C言語で書かれたカーネルでも多態性とモジュール性を実現し、柔軟なシステム設計を可能にする
  • vtable(仮想関数テーブル)を使ってデバイスやサービスのインターフェースを標準化し、ランタイムでの動的変更によってさまざまな動作をサポート
  • カーネルサービスやスケジューラは、vtableを通じて開始・停止・再起動といった一貫したインターフェースを提供し、実装の詳細をカプセル化
  • カーネルモジュールとの組み合わせにより、動的なドライバーロードをサポートし、再コンパイルなしでシステムを拡張可能
  • このアプローチは柔軟性と実験の自由をもたらす一方、複雑な構文や明示的なオブジェクト受け渡しによる煩雑さが欠点となる

OS開発における自由とオブジェクト指向パターン

  • 自分自身のOS開発では、協業や実運用上の制約なしに自由な実験が可能
    • セキュリティ脆弱性、コード保守、リリース負担から解放される
    • これがOS開発の魅力であり、非標準的なプログラミングパターンを探求できる
  • LWNの記事「Object-oriented design patterns in the kernel」では、LinuxカーネルがCでオブジェクト指向の原則を実装した事例を紹介
    • 関数ポインタを含む構造体で多態性を実現
    • カプセル化、モジュール性、拡張性によって、低レベルなカーネルでもオブジェクト指向の利点を活用

vtableの基本概念

  • vtableは関数ポインタを含む構造体で、オブジェクトのインターフェースを定義する
    • 例: デバイス動作用の構造体
      struct device_ops {  
          void (*start)(void);  
          void (*stop)(void);  
      };  
      struct device {  
          const char *name;  
          const struct device_ops *ops;  
      };  
      
  • 異なるデバイス(例: netdev, disk)が同じAPIを使い、実装は異なる
    • netdev.ops->start()はネットワークデバイス、disk.ops->start()はディスクデバイスの動作を呼び出す
  • ランタイム変更: vtableを動的に差し替えることで、呼び出し側のコードを変えずに動作を変更
    • 適切な同期により、クリーンな動的動作の進化を提供

OSでの適用例

サービス管理

  • カーネルサービス(ネットワークマネージャー、ワーカープール、ウィンドウサーバーなど)を一貫したインターフェースで管理
    • サービス構造体:
      struct service_ops {  
          void (*start)(void);  
          void (*stop)(void);  
          void (*restart)(void);  
      };  
      struct service {  
          pid_t pid;  
          const struct service_ops *ops;  
      };  
      
  • 各サービスは固有の動作を実装しつつ、ターミナルから開始/停止/再起動を標準化された方法で実行
  • コードとサービス間の結合度を低減し、管理を簡素化

スケジューラ

  • スケジューラは、ラウンドロビン、最短ジョブ優先、FIFO、優先度スケジューリングなど多様な戦略をサポート
    • インターフェースはyieldblockaddnextに単純化
    • vtableで定義し、ランタイムにスケジューリングポリシーを切り替え可能
    • カーネルの残りの部分を修正せずに、全体のポリシーを変更可能

ファイル抽象化

  • Linuxのfile_operations構造体は、「すべてはファイル」という哲学を実装
  • ソケット、デバイス、テキストファイルはいずれも同じread/writeインターフェースを提供
  • ユーザー空間のコードは、実装の詳細を知らなくても一貫した方法で動作できる

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

  • カーネルモジュールは、vtableの差し替えによって動的なドライバーやフックのロードをサポート
    • Linuxモジュールのように、再コンパイルや再起動なしでカーネルを拡張可能
    • 新機能追加時には既存構造体のvtableだけを更新

欠点

  • 構文の複雑さ:
    • object->ops->start(object)のように、オブジェクトを明示的に渡す必要がある
    • C++の暗黙的な受け渡しに比べて煩雑
    • 関数シグネチャも冗長:
      static void object_start(struct object* this) {  
          this->id = ...  
      }  
      
  • 利点: 明示的な受け渡しにより関数の依存関係が明確になり、オブジェクトと動作の結び付きが透明になる
    • カーネルコードにおける複雑さと明確さの適切なtradeoff

示唆

  • vtableは柔軟性を保ちながら複雑さを減らすシンプルな方法を提供
    • ランタイムでの動作差し替え、一貫したインターフェースの維持、新機能追加の容易さ
  • C言語でオブジェクト指向設計を実装する新たな方法を示し、OS開発の実験的な面白さを強調
  • 追加資料: xineプロジェクト(https://xine.sourceforge.net/hackersguide/…
  • OS開発は創造的な実験の場であり、オブジェクト指向パターンが低レベルシステムでも強力な道具であることを証明

1件のコメント

 
GN⁺ 2025-08-29
Hacker Newsのコメント
  • LinuxカーネルはCで書かれているにもかかわらず、構造体に関数ポインタを使って多態性を実現するなど、オブジェクト指向の原理を取り入れている、という内容の記事について議論している。こうしたテクニックはオブジェクト指向プログラミングよりはるか以前から存在しており、「抽象データ型(ADT)」またはデータ抽象化と呼ばれる。ADTとOOPの本質的な違いは、ADTでは関数実装を省略できるが、OOPでは常に実装が必要だという点。OOPで任意の関数が必要なら、その任意関数ごとに追加クラスを作らなければならず、実装のたびに多重継承で一緒に継承し、実行時にそのオブジェクトがその追加クラスのインスタンスか確認しなければならない煩雑さがある。一方ADTでは、関数ポインタがNULLかどうかを単純に確認すればよい
    • SmalltalkやObjective-Cでは、実行時にオブジェクトがメッセージに応答できるかを簡単に確認するのが伝統的なOOPのやり方だという。OOPがC++やJavaの過度にクラス中心な設計パターンのせいで本質が歪められてしまったのが残念だとしている
    • おおむね同意で、Cでもこうしたパターンを使うが、伝統的なOOPではベースにデフォルト実装やスタブ実装を入れるのが一般的なアプローチだと言及している。最近のOOPや概念指向言語では、必要なAPIの部分集合だけを使うインターフェースへキャストする方法もある。Go言語がよい例だという
    • このテクニックがオブジェクト指向プログラミングより先だという主張について、むしろOOPは既存のパターンやパラダイムを形式化したものだと表現したいとしている
    • JavaやC#など大半のOOP言語でも、今ではラムダを使えるので、Cとまったく同じように実装できる。ラムダは単なる関数ポインタなので、インスタンス変数に直接代入できる。(Javaがラムダ導入に10年以上かかり、Sun MicrosystemsがかつてMicrosoftを相手にJavaへラムダを追加しようとした試みをめぐって訴訟まで起こしたという、妙な昔話もある)
    • 継承は必須ではなく、コンポジットパターンを使えばよい。Pythonも同様にself/this/objectポインタを明示的に渡す必要があるため、Cスタイルのデータ抽象化に似ている
  • 数年前、PeterpaulがC上で快適に使える軽量オブジェクト指向システムを開発したことがある(repo)。オブジェクトを明示的に渡す必要はなく、ドキュメントは不足しているがフルテストスイートがある(テスト1テスト2
    • carbonの糖衣構文なしではどんな見た目になるのか気になるなら、ここで見られる。パラメトリック多態性はサポートしていないように見える
    • Valaもこのニッチ領域に適した試みをしていると思う
  • 自分はこの点にあまり詳しくないが、OPはカーネル開発者たちがやっていることとは違うことをしているように見える。OPがリンクした記事を読むと、vtableには型付き関数ポインタがあるが、OPはvoidポインタを使っているという印象を受ける。また、カーネル開発者の記事で言及されていた主な利点は、各構造体インスタンスに複数の関数ポインタを持たせず、vtableポインタを1つだけ持たせることでメモリを節約できることだ。つまりメモリ節約が主眼なのに、OPはこのvtableをランタイムでのメソッド差し替えや多態性実現のための間接化に使っている。このパターンはカーネル開発者が言っていた内容とは異なる
    • OPが言っていたのはvoidポインタではなく、void(引数なし・戻り値なしの関数)という意味だ。vtableは多態性を実現するために使うものだ。多態性がなければvtable自体を使わないので、むしろメモリはさらに節約できる
  • オブジェクトを毎回明示的に渡さなければならないのが不便だという意見に対して、自分はむしろ暗黙のthisが嫌いだという。実際にはthisインスタンスをずっと渡しているわけで、明示的なthisなら、その変数がインスタンスに属するのか、グローバルなのか、別の場所から来たのか混乱しない
    • C++(そしてJava)のOOP構文で、インスタンスメンバ参照時にthisを必須にしないのは大きな誤りの一つだと思う
    • 作者は、以下のように object->ops->start(object) でオブジェクトを二度明示しなければならない点を指摘しているのだと思う。1回はvtable解決のため、もう1回はC関数実装にオブジェクトを渡すために必要になる
    • 変数の所属を明確にするため、メンバ変数に mFoom_Foofoo_ などの命名規約をよく使っている。foo_this->foo より簡潔なので好んでいる。もちろんC++でも明示的に this を使うことはできる
    • 暗黙のthisはコーディングをより簡潔にし、実際のメソッドを使えば関数ごとにstruct接頭辞を繰り返す必要もなくなる。たとえば mystruct_dosmth(s); より s->dosmth(); のほうが自然になる
    • マクロを使えばもう少し賢く処理することもできる
  • Tmuxの発表資料(資料)で、Cでこうしたパターンを初めて学んだ。この概念について自分でもまとめた記事がある(tmuxオブジェクト指向コマンドの記事
  • 大学時代にいくつかの小規模プロジェクトでこのやり方を実装したことがある。CでOOPに近い感触を出せるのは楽しかったが、注意しないとすぐに大きな問題になりうる
  • これはオブジェクト全体ではなく、インターフェース(つまりvtable、関数ポインタテーブル)を活用するパターンだという点に注意すべきだ。クラスや継承など、他のオブジェクト指向機能はむしろコストが高く、扱いづらい面が多い
    • 継承とは結局のところvtableの合成形だ。クラスというものも、vtableとスコープ変数の結合にすぎない
    • Cではstructを先頭メンバとしてキャストすると、フィールド継承は思ったより自然にできる
    • vtableには普通thisポインタを受け取る関数が入る。struct file_operations の例はthisポインタを受け取らない関数ポインタなので、厳密には本当のvtableとは言いにくい
  • vtable関数にインラインラッパーを作って、thing->vtable->foo(thing, ...) の代わりに foo(thing, ...) のように書けるようにしている
  • こうしたパターンがなぜ新しいC標準に取り込まれないのか、ずっと不思議に思っていた。明らかに多くの人が同じパターンを繰り返し実装している
    • 糖衣構文(煩雑さを減らすための構文要素)を追加すると、公式に許可された使い方と、何かが足りないようなフォールバック(従来方式)の両方が同時に存在しなければならない。Cの利点は動的な複雑さを隠さないことにある。動的ディスパッチが起きるとき、それが常に明確だ。すでに多くの言語がこうした形式化を提供しているが、C固有の強みは複雑さが露出していることだ。だから本当に動的ディスパッチが必要なときだけ使うようになる。また構文もそれほど難しくない
    • おそらくHigh C Compilerあたりでは、ある程度こういう方向性が試みられていたようだ
  • 絶対にこのパターンを使うな、という強い実体験からの助言だ。この構造で書かれた大規模コードの保守という悪夢を経験した。可読性はひどく、コンパイラはポインタ経由の呼び出しを最適化できず、ツール類の支援もまったくない。構文も不格好で、新人はC++コンパイラの内部を熟知してようやくコードを読めるようになる。何より、OOP導入の疑わしい利点に比べて、長期的には保守性を壊しかねない。本当に必要なら、単にC++を使うべきだ
    • 具体的に何が悪夢だったのかという問いに対し、糖衣構文が少ないほうが、関数呼び出しが動的ディスパッチかどうかをむしろ明確に示すので可読性が高いと考えている。だから動的ディスパッチが必要な箇所にだけ限定して使える。そして、Cのダイナミックなコードは関数ポインタが少ないため最適化しやすい、というブログ記事を読んだこともある。C++コンパイラを丸ごと再実装しろと言っているわけではなく、単にOOPの本質を理解していれば自然に実装できるという話だ。最後に、「Cを粗雑なC++にするな」という主張については、むしろこれこそがCらしいやり方であり、必要な場所にだけ適切なダイナミズムを入れやすい点が選ばれる理由だとしている。