その場でキャラクタータイプを生成(またはRustに夢中)

この記事では、Rustプログラミング言語、特に型オブジェクトを少し楽しみます。


Rustに精通したとき、型オブジェクトの実装の詳細の1つが興味深く思えました。 つまり、仮想関数テーブルはデータ自体の中にあるのではなく、それへの「太い」ポインタの中にあります。 型オブジェクトへの各ポインター)には、データ自体へのポインターと、特定の構造に対してこの型オブジェクトを実装する関数のアドレスが配置される仮想テーブルへのリンクが含まれます(ただし、これは実装の詳細であるため、動作が変更される場合があります)。


太いポインターを示す簡単な例から始めましょう。 次のコードは、64ビットアーキテクチャ8および16で出力されます。


fn main () { let v: &String = &"hello".into(); let disp: &std::fmt::Display = v; println!("  : {}", std::mem::size_of_val(&v)); println!("   -: {}", std::mem::size_of_val(&disp)); } 

なぜこれが面白いのですか? エンタープライズJavaに従事していたとき、かなり定期的に発生したタスクの1つは、既存のオブジェクトを特定のインターフェースに適合させることでした。 つまり、オブジェクトは既に存在し、リンクとして発行されていますが、指定されたインターフェイスに適合させる必要があります。 そして、入力オブジェクトを変更することはできません。それはそれです。


私はこのようなことをしなければなりませんでした:


 Person adapt(Json value) { // ...- , , ,  "value"  //   Person return new PersonJsonAdapter(value); } 

このアプローチにはさまざまな問題がありました。 たとえば、同じオブジェクトが2回「適応」する場合、2つの異なるPerson取得されます(リンク比較の観点から)。 そして、毎回新しいオブジェクトを作成しなければならないという事実は、なんとなくugいものです。


Rustで型オブジェクトを見たとき、Rustではもっとエレガントにできると思いました! 別の仮想テーブルを取得してデータに割り当て、新しい特性オブジェクトを取得できます! また、各インスタンスにメモリを割り当てないでください。 同時に、「借入」のロジック全体がそのまま残ります-適応関数はfn adapt<'a>(value: &'a Json) -> &'a Person (つまり、ソースデータ)。


さらに、同じ型(たとえば、 String )を「強制」して、動作を変えて型オブジェクトを数回実装できます。 なんで? しかし、企業で何が必要なのか決してわかりませんか?


これを実装してみましょう。


問題の声明


タスクを次のように設定します。 annotate関数を作成します。この関数は、次の型オブジェクトを通常の型String 「割り当て」ます。


 trait Object { fn type_name(&self) -> &str; fn as_string(&self) -> &String; } 

そして、 annotate関数自体:


 ///    - `Object`,   , ///   "" -- ,    `type_name`. fn annotate<'a>(input: &'a String, type_name: &str) -> &'a dyn Object { // ... } 

すぐにテストを書きましょう。 まず、「割り当てられた」タイプが予想されるものと一致することを確認します。 次に、元の文字列を取得できることを確認し、同じ文字列になるようにします(ポインターの観点から)。


 #[test] fn test() { let input: String = "hello".into(); let annotated1 = annotate(&input, "Widget"); let annotated2 = annotate(&input, "Gadget"); // -   ,    assert_eq!("Widget", annotated1.type_name()); assert_eq!("Gadget", annotated2.type_name()); let unwrapped1 = annotated1.as_string(); let unwrapped2 = annotated2.as_string(); //       --   assert_eq!(unwrapped1 as *const String, &input as *const String); assert_eq!(unwrapped2 as *const String, &input as *const String); } 

アプローチ番号1:そして少なくとも私たちの後に洪水!


最初に、完全に単純な実装を作成してみましょう。 type_nameを追加で含む「ラッパー」でデータをラップするだけです。


 struct Wrapper<'a> { value: &'a String, type_name: String, } impl<'a> Object for Wrapper<'a> { fn type_name(&self) -> &str { &self.type_name } fn as_string(&self) -> &String { self.value } } 

まだ特別なものはありません。 すべてはJavaのようなものです。 しかし、ガベージコレクタはありません。このラッパーはどこに保存しますか? リンクを返す必要があるので、 annotate関数を呼び出した後もリンクが有効なままになります。 Box恐ろしいものを入れて、 WrapperヒープWrapper強調表示されるようにします。 そして、それへのリンクを返します。 そして、 annotate関数を呼び出した後もラッパーが生きたままになるように、このボックスを「リーク」します。


 fn annotate<'a>(input: &'a String, type_name: &str) -> &'a dyn Object { let b = Box::new(Wrapper { value: input, type_name: type_name.into(), }); Box::leak(b) } 

...そしてテストに合格しました!


しかし、これは疑わしい決定です。 「注釈」ごとにメモリを割り当てるだけでなく、メモリリークがBox::leakます( Box::leakは、ヒープに格納されているデータへのリンクを返しますが、同時にボックス自体を「忘れる」、つまり、自動解放は行われません)


アプローチ2:アリーナ!


まず、これらのラッパーをどこかに保存して、ある時点でリリースされるようにします。 ただし、同時にannotate署名をそのまま保持します。 つまり、参照カウント(たとえば、 Rc<Wrapper> )でリンクを返すことはできません。


最も簡単なオプションは、これらのラッパーを格納する「型システム」である補助構造を作成することです。 そして、終了したら、この構造とそれを含むすべてのラッパーをリリースします。


そのようなもの。 typed-arenaライブラリはラッパーを格納するために使用されますが、 Vec<Box<Wrapper>>使用して取得できます。主なことは、 Wrapperがどこにも移動しないことを保証することです(夜には、これにpin APIを使用できます):


 struct TypeSystem { wrappers: typed_arena::Arena<Wrapper>, } impl TypeSystem { pub fn new() -> Self { Self { wrappers: typed_arena::Arena::new(), } } ///     `input`,      , ///    (  ,    , ///        )! pub fn annotate<'a: 'b, 'b>( &'a self, input: &'b String, type_name: &str ) -> &'b dyn Object { self.wrappers.alloc(Wrapper { value: input, type_name: type_name.into(), }) } } 

しかし、 Wrapperタイプのリンクのライフタイムの原因となったパラメーターはどこに行きましたか? タイプtyped_arena::Arena<Wrapper<'?>>固定ライフタイムをtyped_arena::Arena<Wrapper<'?>>ことができないため、これを取り除く必要がありました。 各ラッパーには、 inputに応じて一意のパラメーターがありinput


代わりに、lifetime-timeパラメーターを取り除くために、少し安全でないRustを振りかけます:


 struct Wrapper { value: *const String, type_name: String, } impl Object for Wrapper { fn type_name(&self) -> &str { &self.type_name } ///   -- ,     (  /// `annotate`),     (    - /// `&Object`)  ,      (`String`). fn as_string(&self) -> &String { unsafe { &*self.value } } } 

そして、テストは再び合格し、それにより決定の正確性に自信を与えます。 unsafeunsafeと不快に感じることに加えて(そうであるべきであるが、安全でないRustと冗談を言わない方が良い!)。


しかし、それでも、ラッパーに追加のメモリ割り当てを必要としない約束されたオプションについてはどうでしょうか?


アプローチ#3:地獄の門を開く


アイデア。 一意の「タイプ」(「ウィジェット」、「ガジェット」)ごとに、仮想テーブルを作成します。 プログラムの実行中の手。 そして、それ自体をデータ自体によって与えられたリンクに割り当てます(思い出すように、これは単にString )。


まず、取得する必要があるものの簡単な説明。 それでは、型オブジェクトへの参照は、どのように配置されますか? 実際、これらはデータへのポインターと仮想テーブルへのポインターの2つにすぎません。 だから私たちは書く:


 #[repr(C)] struct TraitObject { pub data: *const (), pub vtable: *const (), } 

#[repr(C)]メモリ内の正しい場所を保証する必要があります)。


すべてが単純なようです。指定されたパラメーターの新しいテーブルを生成し、型オブジェクトへのリンクを「収集」します! しかし、この表は何で構成されていますか?


この質問に対する正しい答えは、「これは実装の詳細です」です。 しかし、そうします。 プロジェクトのルートにrust-toolchainファイルを作成し、そこに書き込みます: nightly-2018-12-01 。 結局、固定アセンブリは安定していると見なすことができますよね?


Rustバージョンを修正したので(実際、以下のライブラリの1つに対してナイトリービルドが必要になります)。


インターネットでいくつかの検索を行った後、テーブル形式が単純であることがわかります:最初にデストラクタへのリンクがあり、次にメモリの割り当てに関連付けられた2つのフィールド(タイプサイズとアライメント)があり、関数が次々に行きます(順序はコンパイラの裁量ですが、 2つの関数のみであるため、推測の確率はかなり高く、50%です)。


だから私たちは書く:


 #[repr(C)] #[derive(Clone, Copy)] struct VirtualTableHeader { destructor_fn: fn(*mut ()), size: usize, align: usize, } #[repr(C)] struct ObjectVirtualTable { header: VirtualTableHeader, type_name_fn: fn(*const String) -> *const str, as_string_fn: fn(*const String) -> *const String, } 

同様に、メモリ内の正しい場所を保証するに#[repr(C)]必要です。 私は2つの構造に分割しましたが、少し後でそれが役に立ちます。


次に、 annotate機能を提供する型システムを作成してみましょう。 生成されたテーブルをキャッシュする必要があるので、キャッシュを取得しましょう。


 struct TypeInfo { vtable: ObjectVirtualTable, } #[derive(Default)] struct TypeSystem { infos: RefCell<HashMap<String, TypeInfo>>, } 

TypeSystem::annotate関数が共有リンクとして受信&selfできるように、 RefCellの内部状態を使用します。 TypeSystemから「借用」して、生成した仮想テーブルがannotateから返す型オブジェクトへの参照よりも長くTypeSystemするようにするため、これは重要です。


多くのインスタンスに注釈を付けることができるようにしたいため、可変リンクとして&mut selfを借用して使用することはできません。


そして、このコードをスケッチしましょう:


 impl TypeSystem { pub fn annotate<'a: 'b, 'b>( &'a self, input: &'b String, type_name: &str ) -> &'b dyn Object { let type_name = type_name.to_string(); let mut infos = self.infos.borrow_mut(); let imp = infos.entry(type_name).or_insert_with(|| unsafe { //    ,  ? let vtable = unimplemented!(); TypeInfo { vtable } }); let object_obj = TraitObject { data: input as *const String as *const (), vtable: &imp.vtable as *const ObjectVirtualTable as *const (), }; //       - unsafe { std::mem::transmute::<TraitObject, &dyn Object>(object_obj) } } } 

このテーブルはどこから入手できますか? その最初の3つのエントリは、指定されたタイプの他の仮想テーブルのエントリと一致します。 したがって、それらを取得してコピーするだけです。 まず、このタイプを取得しましょう:


 trait Whatever {} impl<T> Whatever for T {} 

この「他の仮想テーブル」を取得することは有用です。 次に、これら3つのエントリを彼からコピーします。


 let whatever = input as &dyn Whatever; let whatever_obj = std::mem::transmute::<&dyn Whatever, TraitObject>(whatever); let whatever_vtable_header = whatever_obj.vtable as *const VirtualTableHeader; let vtable = ObjectVirtualTable { //  ! header: *whatever_vtable_header, type_name_fn: unimplemented!(), as_string_fn: unimplemented!(), }; TypeInfo { vtable } 

基本的に、 std::mem::size_of::<String>()およびstd::mem::align_of::<String>()介してサイズとアライメントを取得できます。 しかし、どこからデストラクタが「盗まれる」ことができるのかはわかりません。


わかりましたが、これらの関数type_name_fnas_string_fnアドレスはどこで取得しますか? as_string_fnは一般に必要ではなく、データポインターは常にタイプオブジェクトの表現の最初のレコードとして使用されます。 つまり、この関数は常に同じです。


 impl Object for String { // ... fn as_string(&self) -> String { self } } 

しかし、2番目の機能では、それほど簡単ではありません! また、名前 "type"、 type_nameにも依存します。


関係ありません。実行時にこの関数を生成するだけです。 dynasmライブラリーを使用してみましょう(現時点では、Rustナイトリービルドが必要です)。 について読む
関数呼び出し規約


簡単にするために、Mac OSとLinuxのみに関心があると仮定します(これらの楽しい変換のすべての後、互換性はもう気になりませんよね?)。 そして、もちろん、排他的にx86-64です。


2番目の関数as_string 、実装が簡単です。 最初のパラメータはRDIレジスタにあることが約束されています。 そして、値をRAX返します。 つまり、機能コードは次のようになります。


 dynasm!(ops ; mov rax, rdi ; ret ); 

しかし、最初の関数は少し複雑です。 まず、 &strを返す必要があり&strが、これは太いポインタです。 最初の部分は文字列へのポインタで、2番目の部分は文字列スライスの長さです。 幸いなことに、上記の規則により、2番目の部分にEDXレジスタを使用して128ビットの結果を返すことができます。


type_name文字列を含む文字列スライスへのリンクをどこかに取得するために残っています。 type_nameに依存することは望ましくありません(ただし、存続期間の注釈を通じて、 type_nameが戻り値よりも長く存続することを保証できます)。


しかし、この行のコピーがあり、それをハッシュテーブルに入れます。 指を交差させて、 String::as_strが返す文字列スライスの位置は、文字String移動によって変化しないとString::as_strます(そして、この文字列がキーによって保存されるHashMapサイズ変更プロセス中にStringが移動します)。 標準ライブラリでこのような動作が保証されているかどうかはわかりませんが、単純にプレイしていますか?


必要なコンポーネントを取得します。


 let type_name_ptr = type_name.as_str().as_ptr(); let type_name_len = type_name.as_str().len(); 

そして、この関数を書きます:


 dynasm!(ops ; mov rax, QWORD type_name_ptr as i64 ; mov rdx, QWORD type_name_len as i64 ; ret ); 

そして最後に、最終的なannotateコード:


 pub fn annotate<'a: 'b, 'b>(&'a self, input: &'b String, type_name: &str) -> &'b Object { let type_name = type_name.to_string(); //       let type_name_ptr = type_name.as_str().as_ptr(); let type_name_len = type_name.as_str().len(); let mut infos = self.infos.borrow_mut(); let imp = infos.entry(type_name).or_insert_with(|| unsafe { let mut ops = dynasmrt::x64::Assembler::new().unwrap(); //     `type_name` let type_name_offset = ops.offset(); dynasm!(ops ; mov rax, QWORD type_name_ptr as i64 ; mov rdx, QWORD type_name_len as i64 ; ret ); //     `as_string` let as_string_offset = ops.offset(); dynasm!(ops ; mov rax, rdi ; ret ); let buffer = ops.finalize().unwrap(); //      let whatever = input as &dyn Whatever; let whatever_obj = std::mem::transmute::<&dyn Whatever, TraitObject>(whatever); let whatever_vtable_header = whatever_obj.vtable as *const VirtualTableHeader; let vtable = ObjectVirtualTable { header: *whatever_vtable_header, type_name_fn: std::mem::transmute(buffer.ptr(type_name_offset)), as_string_fn: std::mem::transmute(buffer.ptr(as_string_offset)), }; TypeInfo { vtable, buffer } }); assert_eq!(imp.vtable.header.size, std::mem::size_of::<String>()); assert_eq!(imp.vtable.header.align, std::mem::align_of::<String>()); let object_obj = TraitObject { data: input as *const String as *const (), vtable: &imp.vtable as *const ObjectVirtualTable as *const (), }; unsafe { std::mem::transmute::<TraitObject, &dyn Object>(object_obj) } } 

dynasm目的のためにdynasm bufferフィールドをTypeInfo構造に追加する必要もあります。 このフィールドは、生成された関数のコードを保存するメモリを管理します。


 #[allow(unused)] buffer: dynasmrt::ExecutableBuffer, 

そして、すべてのテストに合格しました!


終わりましたマスター


簡単かつ自然に、Rustコードで型オブジェクトの独自の実装を生成できます!


後者のソリューションは、実装の詳細に積極的に依存しているため、使用を推奨していません。 しかし実際には、あなたがしなければならないことをしなければなりません。 絶望的な時代には必死の手段が必要です!


ただし、ここでは、1つ(複数)の機能に依存しています。 つまり、テーブルを使用している型オブジェクトへの参照がなくなった後、テーブルが実質的に占有しているメモリを解放しても安全です。 一方では、タイプオブジェクトの参照を介してのみ仮想テーブルを使用できることは論理的です。 一方、Rustが提供するテーブルの寿命は'staticです。 いくつかの目的のためにテーブルをリンクから分離するコードを想定することは完全に可能です(たとえば、 いくつかの汚いトリックのためにあなたは決して知りませ )。


ソースコードはこちらにあります



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


All Articles