Skip to content

OpeLa Internal Design

Kota UCHIDA edited this page May 20, 2021 · 23 revisions

OpeLa コンパイラの内部設計

OpeLa メインページ

C++ の機能をどこまで使うか

OpeLa コンパイラは、セルフホストを達成するまでは C++ で記述します。 C++ の素晴らしい機能、ライブラリを活用して作りたいところですが、最終的に OpeLa 言語に移植することを考えるとバランスが大切です。 移植をスムーズにするためには、OpeLa コンパイラを実装するのに利用した C++ の言語機能やライブラリを、OpeLa 言語でも実現する必要があるからです。

OpeLa 言語には、MikanOS を実装するために欲しい言語機能とライブラリを用意したいと考えています。 したがって、OpeLa コンパイラもその範囲で実装するのが良いだろうと思います。 OpeLa コンパイラ V1 は、何も制約を考えずに実装していましたが、V2 はこれらの制約を踏まえて実装します。

OpeLa コンパイラ V2 を実装するのに利用する C++ の機能を考えるために、MikanOS の実装に活用している機能、ライブラリを洗い出しました。

  • int や char 等の整数型、静的配列
  • グローバル変数、ローカル変数
  • 関数
  • ポインタ演算、関数ポインタ経由の関数呼び出し
  • 可変長引数 ...
  • 構造体、クラス、継承
  • alignas
  • インラインアセンブラ
  • 無名関数、クロージャ
  • string, vector, map, deque

テンプレートをどうするか

MikanOS では、テンプレートを一部で使っています。この処理を、OpeLa 言語にどうやって移植するかを検討してみます。

テンプレートを効果的に使っている例として MemMapRegister というクラステンプレートを見てみます。

template <typename T>
class MemMapRegister {
 public:
  T Read() const {
    T tmp;
    for (size_t i = 0; i < len_; ++i) {
      tmp.data[i] = value_.data[i];
    }
    return tmp;
  }

  void Write(const T& value) {
    for (size_t i = 0; i < len_; ++i) {
      value_.data[i] = value.data[i];
    }
  }

 private:
  volatile T value_;
  static const size_t len_ = ArrayLength<decltype(T::data)>::value;
};

このクラスは、メモリマップされたレジスタを読み書きするためのラッパクラスです。メモリマップされたレジスタの読み書きは、コンパイラ最適化の影響を避けるため volatile でアクセスする必要があります。さらに、ハードウェアの仕様で、アクセスするビット幅が決まっていることがよくあります。例えば xHCI の 64 ビットレジスタは、64 ビット幅で読み書きする必要があります。このクラスを使うと、そのようなアクセスを守らせることができます。レジスタを読み書きしたい場合に、誤って volatile 無しでアクセスしてしまったり、64 ビット幅で読まずにビットフィールドを直接読んでしまう、というようなことを防ぎます。

MemMapRegister クラスが提供する機能を、テンプレート無しの OpeLa 言語で実現する方法を考えてみます。

次に usb/xhci/ring.hpp の Ring クラス を見てみます。このクラスはテンプレートを用いた Push メソッドを持っています。

  class Ring {
   public:
    ≪中略≫
    template <typename TRBType>
    TRB* Push(const TRBType& trb) {
      return Push(trb.data);
    }
    ≪中略≫

   private:
    ≪中略≫
    TRB* Push(const std::array<uint32_t, 4>& data);
    ≪中略≫
  };

テンプレート引数 TRBType は、いくつもある「○○TRB」という構造体を受け取る意図があります。TRB 構造体の 1 種である NormalTRB の定義は次のようになっています。

  union NormalTRB {
    static const unsigned int Type = 1;
    std::array<uint32_t, 4> data{};
    struct {
      ≪中略(各種のフィールドの定義)≫
    } __attribute__((packed)) bits;

    ≪中略≫
  };

最も外側は共用体になっていて、TRB というデータ構造を単なる 32 ビット整数の配列としてアクセスすることもできるし、bits 経由でフィールドを名前でアクセスすることもできる、という設計です。

関数テンプレートとして実装された 1 つ目の Push メソッドは、TRB を 32 ビット整数の配列として共通化して 2 つ目の Push に受け渡すというだけです。2 つ目の Push は、受け取った配列をリングキューの末尾に追加します。

なぜ、このような 2 段構えになっているかというと、コードサイズの削減のためです。テンプレートを使った実装では複数の型に対応できる利点がありますが、型の数だけコードがコピーされるため、機械語の量が増えてしまいます。仮に 2 段構えをやめて、1 つ目の Push 自身がリングキューへの追加をするように作ると、リングキューへの追加処理が TRB の種類の数だけコピーされるわけです。一方、2 段構えにしておけば、コピーされるのは 2 つ目の Push を呼び出すコードだけで、リングキューへの追加処理の機械語は 1 つだけ出力されます。

Push の例では、テンプレートの除去は簡単です。テンプレートを使わない Push を public して、呼び出し側で Push(trb.data) というように、.data を明示的に書いてあげれば良いですね。たいした手間ではないでしょう。

Ver.1 の設計

以下は V1 の設計です。V2 ではデータ構造が変わるかもしれません。

Type 構造体の仕様

直感的に分かるように、具体例により仕様を記述します。

配列型 kArray

配列型(kind: kArray)は、base に要素の型、num に要素数が設定される。

関数型 kFunc

関数型(kind: kFunc)は、next に引数型のリストの先頭要素、base に戻り値型が設定される。 引数型リストの最後には可変長引数を表す kVParam が来得る。

構造体型 kStruct

構造体型(kind: kStruct)は、next にフィールド型のリストの先頭要素が設定される。 フィールド型は name にフィールド名が設定される。

Clone this wiki locally