任意のプログラムを軽量プロセスとして起動できるDeskVM
言語開発 Advent Calendar 2022の10日目の記事です。
こんなことないでしょうか
- 自分の好きな言語は軽量プロセスなどの非同期機能がないので非同期プログラミングに困っている
- 自分の好きな言語のスケジューラーがコーナーケースでデッドロックしたりメモリリークしたりして困っている
- 自分の好きな言語にはメッセージパッシングの機能がないけどメッセージパッシングやってみたい
このような場合Erlang VMの上で自分の好きな言語を動かしたいなという気持ちが芽生えてくるわけです。
しかし、当たり前ですが、Erlang VMはErlangのプログラムしか動かせません。
全部をErlangで書けばいいという話かも知れませんが、現実問題、プログラミング言語には好みがあります。
とくに静的型付けを好む人はErlangを選びづらいと思います。
またシングルスレッドで大量の計算を高速で行いたいプログラムの場合、Erlangで書くにはテクニックが必要です。
また、OSのプロセスとして起動すればいいじゃないかと思うかも知れませんが、それには以下の問題があります。
- プロセスのフットプリントが大きく、大量のプロセスを起動するのには向いていない
- メッセージパッシング(IPC)の方法がOSによって違うためクロスプラットフォーム対応が難しい
- ブラウザからOSプロセスは基本的には起動できないのでブラウザ上で動くアプリを作れない
とくに最後が現代においてはクリティカルだと思います。
こういった問題を全て解決してしまうのが現在開発中のDeskVMというソフトウェアです。
なんでDesk?
DeskVMという名前はもともとDesk言語という言語向けのVMとして設計を始めたことに由来します。
しかし、設計しているうちにDesk言語関係なく最強のVMなのではないかと気づきました。
DeskVMの特徴
- ネイティブでもブラウザでも動く
- 任意のインタプリタ言語を軽量プロセスとして起動できる
- (ネイティブ実行の場合)プログラムをOSプロセスとして起動したい言語(CやRustなど)をあたかも軽量プロセスかのように起動できる
- Erlang VMの基本的な機能が揃っている
- 任意のスケジューラを書くことができる
- シンプルなコードで実装されている
どうでしょうか。最高のVMだと思いませんか?
以下では各特徴がどのように実現されているかを説明します。
1. ネイティブでもブラウザでも動く
プラットフォーム依存のないRustコードで書いてます。
ただ、no_std
環境(組み込み)はやり方があまり分かってなくて対応していません。助けてください。
2. 任意のインタプリタ言語を軽量プロセスとして起動できる
これはトレイト(他の言語で言うインターフェース)によって実現されています。
具体的には以下のInterpreter
トレイトを実装した任意のものを軽量プロセスとして起動できます。
pub trait Interpreter {
fn reduce(&mut self, target_duration: &Duration) -> Result<InterpreterOutput>;
fn effect_output(&mut self, value: Value);
}
reduce
は指定された時間の間だけインタプリタを進めるメソッドです。
Erlang VMだとリダクションという単位を使ってプリエンプティブスケジューリングを行いますが、代わりに、Desk言語では時間を共通の単位として使います。
effect_output
はエフェクトの出力を受け取るためのメソッドです。
DeskVMではエフェクトという統一の仕組みを使ってさまざまなものを表現します。
エフェクトで表現されるもの
- 例外処理
- 軽量プロセスの起動や終了
- メッセージパッシング
- Pub/Sub
- 軽量プロセスのリンク・モニター・名前の登録・フラグの取得更新など
- タイマー
- 軽量プロセスに紐づくKVSの操作
- VM情報の取得
3. (ネイティブ実行の場合)プログラムをOSプロセスとして起動したい言語(CやRustなど)をあたかも軽量プロセスかのように起動できる
上でこう書きました。
具体的には以下の
Interpreter
トレイトを実装した任意のものを軽量プロセスとして起動できます。
つまり、トレイトの実装の中で、起動したOSプロセスとIPC(プロセス間通信)でやりとりすればいいだけです。
難しそうに見えて実は簡単な話でした。
4. Erlang VMの基本的な機能が揃っている
これはErlang VMの実装を参考にしてデータ構造などを設計しています。
例えば以下はDProcess
(DeskVMでは軽量プロセスのことをd-process
と呼ぶ)の定義です。
(高速なプロトタイピングのためにロックを多用していますがいずれはロックフリーなデータ構造にしたいです)
pub struct DProcess {
pub id: DProcessId,
/// An interpreter.
interpreter: RwLock<Box<dyn Interpreter>>,
/// Metadatas mainly used by the scheduler.
metas: RwLock<Metas>,
/// Effect handlers for this d-process.
effect_handlers: RwLock<EffectHandlers>,
/// The status of this d-process.
status: RwLock<DProcessStatus>,
/// Received messages.
mailbox: RwLock<HashMap<Type, VecDeque<Value>>>,
/// Which processor is this d-process attached to.
processor_attachment: RwLock<ProcessorAttachment>,
/// A key-value store for this d-process.
kv: RwLock<HashMap<Type, Value>>,
/// This d-process's flags.
flags: RwLock<DProcessFlags>,
/// Attached timers with the name of the counter used for the label of the event.
timers: RwLock<HashMap<String, Timer>>,
/// A set of d-process ids that are monitoring this d-process.
monitors: RwLock<HashSet<DProcessId>>,
links: RwLock<HashSet<DProcessId>>,
}
5. 任意のスケジューラを書くことができる
これもトレイトです。標準のスケジューラはErlang VMのそれと同等なものを目指して実装されますが、それとは違ったものも実装することができます。
それには以下のようなScheduler
トレイトやMigrationLogic
トレイトを実装するだけです。
pub trait Scheduler {
fn reduce(&mut self, vm: VmRef, processor: &Processor, target_duration: &Duration);
fn attach(&mut self, dprocess: Arc<DProcess>);
fn detach(&mut self, process_id: &DProcessId);
fn notify_status(&mut self, status_update: &StatusUpdate);
}
Scheduler
はDeskVM内に複数ある仮想プロセッサごとにアタッチされるものです。つまり、仮想プロセッサごとに別々のストラテジーでスケジューリングできます。
各メソッドの説明
-
reduce
: スケジューラーの本体、プリエンプティブスケジューリングを行う -
attach
: 軽量プロセスが仮想プロセッサにアタッチされたときに呼ばれる -
detach
: 軽量プロセスが仮想プロセッサからデタッチされたときに呼ばれる -
notify_status
: 軽量プロセスの状態変化時に呼ばれる
次はMigrationLogic
です。
pub trait MigrationLogic {
fn suggest_migration(&mut self, vm: VmRef) -> Vec<MigrateSuggestion>;
fn notify_new_dprocess(&mut self, dprocess_id: &DProcessId);
fn notify_deleted_dprocess(&mut self, dprocess_id: &DProcessId);
fn notify_status(&mut self, status_update: &StatusUpdate);
fn notify_new_processor(&mut self, processor_name: &ProcessorName);
fn notify_deleted_processor(&mut self, processor_name: &ProcessorName);
}
名前はErlang VMにおけるMigration Logicの丸パクリです。
これは仮想プロセッサ間で軽量プロセスを移動させるためのプログラムになります。
新しく起動された軽量プロセスを仮想プロセッサにアタッチしたり、仮想プロセッサごとの軽量プロセスの数が不平等になった場合に移動したりします。
各メソッドの説明
-
suggest_migration
: 本体であり、プロセスの移動を提案する -
notify_new_dprocess
: 新しい軽量プロセスが起動したときに呼ばれる -
notify_deleted_dprocess
: 軽量プロセスが削除されたときに呼ばれる -
notify_status
: 軽量プロセスの状態変化時に呼ばれる -
notify_new_processor
: 新しい仮想プロセッサが作成されたときに呼ばれる -
notify_deleted_processor
: 仮想プロセッサが削除したときに呼ばれる
6. シンプルなコードで実装されている
これは、私が10年以上のプログラミング人生の全てをかけてクリーンでシンプルなコードを書いているというだけの話です。
おわりに
いかがでしたか?
DeskVMはいくつかのユニットテストが通っただけで、まだまだ開発中です。
開発に参加したい方や試しに使ってみたい方がいれば連絡ください。
Discussion