CのLimbo用モジュールの開発(パート1)

Cで記述されたLimboのモジュールは、OS Infernoドライバーとも呼ばれます。 OSカーネルに組み込まれています。 このようなモジュールの必要性は通常、Infernoにない機能をLimboに追加する(既存のサードパーティC / C ++ライブラリを接続して、ホスト固有のsyscallへのアクセスを提供する)か、最大限のパフォーマンスを引き出すことを望む(私の観察によると、 JITとCを有効にしたLimboの間の速度は約1.3〜1.5倍ですが、これが重要になる場合があります。

内容



カーネルにモジュールを埋め込む


残念ながら、InfernoにはCで実装されたモジュールを動的にロードする方法がないため、OSカーネルに直接組み込む必要があります。 (変更ごとにInfernoを再構築する必要があるため、これにより開発が少し複雑になります。幸いなことに、必要な部分的な再構築には約10秒かかります。)

モジュールを埋め込むには、いくつかのファイルを変更する必要があります: libinterp/mkfilemodule/runt.memu/Linux/emuおよびemu/Linux/emu-g 。 また、新しいモジュールはそれぞれ同じ場所にある同じファイルに統合しようとするため、ユーザーはそのようなモジュールをいくつか、未知の順序で追加したい場合があるため、標準patchコマンドは必要な変更を加えることができません。 彼女は1つまたは2つのモジュールを追加しますが、次のことで問題が発生します これらのファイルの編集可能な場所は、彼女が見ると予想しているものと大きく異なり始めます。

この問題を解決するために、Perlでスクリプトをスケッチしました。ほとんどの場合、行に追加するモジュールの名前を変更するだけで十分です。
 my $MODNAME = 'CJSON'; 

また、OS Infernoカーネルにモジュールを埋め込むことにより、上記のすべてのファイルに必要な変更を加えます。 より複雑な場合、たとえば、追加のC / C ++ライブラリをInfernoに接続する必要がある場合、このスクリプトをニーズに合わせて変更する必要があります(C ++ライブラリre2を接続するためのこのような変更の例はRe2モジュール確認できます )。 -Rオプションを使用してスクリプトを実行すると、変更をロールバックできます。

そのため、スクリプトをダウンロードし、 $INFERNO_ROOT入れて、名前を$INFERNO_ROOTに変更し、モジュール名を「Example」に変更して実行します。 これで(まだ存在しない)Exampleモジュールがカーネルに接続され、それを作成してInfernoを再構築します。

まず、2つのファイルを作成します。
  1. module/example.m
     Example: module { PATH: con "$Example"; }; 
  2. libinterp/example.c
     #include <lib9.h> #include <isa.h> #include <interp.h> #include "runt.h" #include "examplemod.h" void examplemodinit(void) { builtinmod("$Example", Examplemodtab, Examplemodlen); } 

OS Infernoの再構築を開始します。
 $ (cd libinterp/; mk nuke) $ rm Linux/386/bin/emu # work around "text file busy" error $ mk install 

これで、モジュールを正常にロードするプログラムをLimboで作成できますが、これまでのところ何の役にも立ちません。
そして実行:
 $ emu ; limbo testexample.b ; testexample Example module loaded ; 

仕組み

アセンブリプロセス中に、 module/example.mファイルが分析され、このモジュールを記述する必要なC構造が生成されます—別のlibinterp/examplemod.h —そのパブリックインターフェイス全体(定数、adt-shki、関数)がlibinterp/runt.hファイルに追加されますすべてのCモジュールに関する情報を含むlibinterp/runt.h 。 これらの2つの.hファイルは、すでにlibinterp/example.c接続されていlibinterp/example.c

さらに、OS Infernoのブートプロセス中にexamplemodinit()関数が1回呼び出され、モジュールのグローバルデータ(存在する場合)を初期化し、( builtinmod(…)呼び出してbuiltinmod(…) Infernoコアに接続する必要があります。 builtinmod()の呼び出しは、モジュールと、 loadコマンドでこのモジュールをロードするときにLimboから使用されるPATH定数で指定された$ Example疑似パスとの接続を確立します。

関数:パラメーターを受け取り、結果を返す


数字

リンクを扱う例が複雑にならないように、単純なデータ型から始めましょう。

Infernoを再構築します。


サンプルを実行する前にemuを再起動することを忘れないでください。
現在実行中のemuは、変更されたCモジュールが含まれていません。
 $ emu ; limbo testexample.b ; testexample Example module loaded increment(5) = 6 ; 

仕組み

アセンブリ中に、 module/example.mにあるincrement()関数について、この関数の説明、パラメーター、および戻り値がファイルlibinterp/runt.hに自動的に追加されました。
 void Example_increment(void*); typedef struct F_Example_increment F_Example_increment; struct F_Example_increment { WORD regs[NREG-1]; WORD* ret; uchar temps[12]; WORD i; }; 

私はまだregs何であるかを理解していません。 アライメントのために明示的に追加されたtemp; retは戻り値へのポインタです。 そして、 iは私たちのパラメータです。


収集、再起動、確認:
 $ emu ; limbo testexample.b ; testexample Example module loaded increment(5) = 6 Hello! ; 

仕組み

libinterp/runt.h取得したものをlibinterp/runt.hます。
 void Example_say(void*); typedef struct F_Example_say F_Example_say; struct F_Example_say { WORD regs[NREG-1]; WORD noret; uchar temps[12]; String* s; }; 

noretでは、 ret代わりにすべてが明確であり、 say()関数は何も返しません。 String*タイプは、Limbo文字列のC実装です。 struct Stringinclude/interp.h 、文字列を操作する関数string2c()この例で使用されているstring2c()など)はlibinterp/string.cます。

他のLimboデータ型を使用した作業は、 Array*List*などを介して同様の方法で実装されます。 すべての構造体に文字列を操作するための既製の補助関数があるわけではありませんが、 libinterp/xec.c仮想マシンのオペコードの実装で十分な例を見つけることができます(たとえば、配列のスライスの操作方法)。

module/example.m宣言されたカスタムadtは、通常のC-shy構造体に変換されmodule/example.m (そして、adtをunionに選択します)。 タプルも通常の構造体に変換されます。

ほとんどの場合module/example.m libinterp/runt.hlibinterp/runt.hlibinterp/runt.hするためにアセンブリを開始する必要があります(これは誤って失敗します)。データ用に作成された構造を正確に確認し、 libinterp/example.cでそれらを使用して実装する方法を理解しlibinterp/example.c

例外

例外をスローするには、単にerror()関数を呼び出します。 raise.hraise.hlibinterp/raise.c説明されている標準エラーを返すか、同じ方法でlibinterp/example.cで独自のエラーを宣言できます。

もちろん、 malloc()使用して自分でメモリを割り当てた場合、 error()を呼び出す前にこのメモリを解放する必要があります。そうしないとリークが発生します。 ヒープを介して標準的な方法で割り当てられたオブジェクト( String*Array* )は、少し後でガベージコレクタによって検出および削除されるため、解放する必要はありません。 ( パート2のヒープおよびガベージコレクターの動作の詳細

戻るリンク

関数から結果を返す暗黙の瞬間の1つは、 *f->ret 、正常に完了した後に関数の実行結果が配置されるメモリセルを物理的に指すという事実に関連しています。 これから2つの結果が続きます。
  1. 最初に結果を*f->retに入れてから、エラーが発生して例外をスローすると判断した場合、Limboの観点からは不可能なことが起こります。And関数は値Andを返し、例外をスローします。
  2. 関数の結果が返される変数にすでに値が含まれている場合(この変数の型は関数によって返される値と同じであるため、もちろん参照でもあります)、メモリから解放する必要がありますあなたとこのリンクを書き換える方法。
最初の問題を実証するために、関数を変更しましょう
increment()このように:

 ; testexample ... catched: some error i = 6 ; 

C関数の2番目の問題を解決するには、戻り値を*f->retに保存する前に、現在の値を解放する必要があります。 これは通常、次のように行われます。
 destroy(*f->ret); *f->ret = new_value; 

または( HはLimbo-nils nilのC番目の類似体です):
 void *tmp; ... tmp = *f->ret; *f->ret = H; destroy(tmp); ... *f->ret = new_value; 

私が理解しているように、現時点ではこれらのオプションに違いはありませんが、Disが複数のCPU /コアで同時に動作するように書き換えられた場合、2番目のオプションは正しく動作しますが、最初のオプションは動作しません。

ブロックを解除


InfernoはグローバルDisロックを使用します(おそらくPythonで広く知られているGILと同様です)。 C-shnyh関数は、ロックが設定された状態で呼び出されます。 Disロック(つまり、C関数のパラメーターと戻り値を含むLimboからアクセス可能な値と変数)を設定ロックのみで操作しても安全です。

ただし、関数が何らかの長い操作を実行する必要がある場合(たとえば、外部ライブラリからの「重い」関数の読み取り/書き込みまたは呼び出し、または長い計算の実行)、この操作の前に、Disが別のスレッドで実行されるようrelease()ロックをrelease()する必要があります関数と並行して、 acquire()再度配置します(そうしないと、結果を返してこの関数を呼び出したLimboコードに戻ることができなくなります)。 例は、 emu/port/inferno.c sys->read()実装にありemu/port/inferno.c
 void Sys_read(void *fp) { ... release(); *f->ret = kread(fdchk(f->fd), f->buf->data, n); acquire(); } 


パート2

Source: https://habr.com/ru/post/J118665/


All Articles