Rust FFIプログラミング-Rustエクスポート共有ライブラリ03


今回は,Rust言語の基本特性からCへのマッピングに注目する.
Rust言語は多汎式(ハイブリッド汎式)の言語であり、命令式(プロセス式)のプログラミングもできるし、オブジェクト向けのプログラミングもできるし、関数式のプログラミングもできる.Rustを単純に何らかの汎式のプログラミング言語に分類するのは、あまり適切ではない.RustはRustである.
C言語は比較的伝統的なプロセスプログラミング言語であるため,RustからCへの変換には,直接的に対標できないものがある.そこで、このようなマッピング作業をするには、追加の仕様や約束が必要です.
ここでは、次の点に注目します.
  • 構造体の方法の処理
  • 汎用の処理
  • Type alias
  • EnumからCへのマッピング
  • 構造体の方法の処理
    Rustでは、構造体(またはenumなど)にメソッドを追加できることを知っています.これはオブジェクト向けのプロパティですが、純粋なCではサポートされていません.そこで、これらのメソッドを個別に関数として実装し、この関数名の前に統一された接頭辞を付けて、次のコードを見なければなりません.
    rustコード
    // rust
    
    #[repr(C)]
    struct Foo {
      a: isize,
      b: isize
    }
    
    impl Foo {
      pub fn method1() {
        ...
      }
    
      pub fn method2(x: isize) -> isize {
        ...
      }
    
      pub fn method3(x: isize, y: isize) -> isize {
        ...
      }
    }
    
    

    このコードがCに翻訳されたとき、対応するのは次のようになります.
    struct Foo {
      int a;
      int b;
    }
    
    void foo_method1(Foo* foo);
    int foo_method2(Foo* foo, int x);
    int foo_method1(Foo* foo, int x, int y);
    
    

    しかし、このマッピングは自動的に変換されず(結局は私たちの約束にすぎない)、手動で書く必要があります.そこで、インタフェース層のRustコードを実現する必要があります.
    // We have struct Foo now
    
    #[no_mangle]
    unsafe extern "C" fn foo_method1(foo: *const Foo) {
      let foo = &*foo;
      foo.method1();
    }
    
    #[no_mangle]
    unsafe extern "C" fn foo_method2(foo: *const Foo, x: isize) -> isize {
      let foo = &*foo;
      foo.method2(x)
    }
    
    #[no_mangle]
    unsafe extern "C" fn foo_method3(foo: *const Foo, x: isize, y: isize) -> isize {
      let foo = &*foo;
      foo.method3(x, y)
    }
    
    

    そして,このインタフェース層コードで動的リンクライブラリをコンパイルし,C側で使えばよい.
    汎用的な処理
    汎用的な処理は少し複雑です.しかし、実際の原理も難しくない.Rustでは,汎用型として静的割当てを指し,またtrait objectを用いて動的割当てを実現する.ここでは,静的配分の解析に焦点を当てた.
    静的割り当てとは,コンパイラがコンパイルする際に,汎用的な具体化タイプに応じて特化展開処理を行うことを意味する.具体的なタイプはいくつかあり、いくつかの異なる特化実装をコピーします(したがって、コード量が増加します).これにより、呼び出し時にポインタを1回ジャンプさせる必要がなくなり、特化された関数/メソッドが直接呼び出されます.したがって、静的割り当ては動的割り当てに比べて、実際には空間で時間を交換し、効率が高くなります.
    したがって,Cに汎用的な方法を導出する際にも,静的割り当ての思考でインタフェース層を実現すればよい.
    実際のコードを見てみましょう.例えば、Rust構造体は次のようになっています.
    #[repr(C)]
    struct Buffer {
      data: [T; 8],
      len: usize,
    }
    
    
    

    方法を実現しました
    impl Buffer {
      pub fn print(&self) {
        ...
      }
    }
    
    

    もし私たちが実際にi 32とf 32の2つのタイプを使ったら.では、FFI層を実現するには、次のように書く必要があります.
    #[no_mangle]
    extern "C" fn buffer_print_i32(buf: Buffer) { ... }
    
    #[no_mangle]
    extern "C" fn buffer_print_f32(buf: Buffer) { ... }
    
    
    

    そして、対応するCのコードは次のようになります.
    struct Buffer_i32 {
      int32_t data[8];
      size_t len;
    };
    
    struct Buffer_f32 {
      float data[8];
      size_t len;
    };
    
    void buffer_print_i32(Buffer_i32 buf);
    void buffer_print_f32(Buffer_f32 buf);
    
    

    FFIのrustにおいて,メソッド名を具体化していることが分かる.Cでは,メソッド名を具体化するほか,タイプを具体化した.このように、C側の非汎用的な悩みに適応しました.
    詳細な読者は、Mつの方法、N種類のタイプがあれば、最後に分けられた関数は、M x N個であることを発見するかもしれません.
    Type alias
    Type aliasはRustではtypeのキーワードを使用していますが、ちょうどCではtypedefというキーワードがあり、似たような機能を果たしています.
    例えば、Rust側には、次のコードがあります.
    // type.rs
    
    #[repr(C)]
    struct Buffer {
      data: [T; 8],
      len: usize,
    }
    
    type IntBuffer = Buffer;
    
    #[no_mangle]
    extern "C" fn buffer_print_int(buf: IntBuffer) { }
    
    
    

    対応するCコードは、次のようになります.
    struct Buffer_i32 {
      int32_t data[8];
      size_t len;
    };
    
    typedef Buffer_i32 IntBuffer;
    
    void buffer_print_int(IntBuffer buf);
    
    

    Type Aliasは、両方のタイプ名をより一致させることができます.
    列挙からCへのマッピング
    Rustでは,列挙は空列挙(Empty Enum),フィールドレス列挙(Fieldless Enum),負荷付き列挙の3つに分類される.
    空の列挙は、enum Foo;という形式を指す.空の列挙には変形はなく、!に等しい空のタイプです.
    フィールド列挙なしは、通常C-like列挙と呼ばれています.バリエーションには追加のデータ/フィールドはありません.
    enum SomeEnum { 
      A, 
      B, 
      C, 
    }
    enum SomeEnum { 
      Variant22 = 22, 
      Variant44 = 44, 
      Variant45, 
    }
    
    

    帯域負荷列挙はRustの特徴であり、変形中にデータ負荷を伴う列挙であり、以下のようなものである.
    enum Foo { 
      Bar(String), 
      Baz, 
    }
    
    

    ここでCとの対応関係を検討する以上、実際にRustが共有ライブラリをCにエクスポートして使用するシーンは、列挙(基本)がFieldless Enumであるため、ここではFieldless EnumからC列挙レイアウトの詳細について説明することに限られる.
    Rustの列挙では、次のようにメモリレイアウトを表示できます.
    #[repr(C)]
    enum SomeEnum { 
      A, 
      B, 
      C, 
    }
    
    

    Rustの列挙で表示できるレイアウトの種類は次のとおりです.
    intビット数レイアウトの指定
  • #[repr(u 8)]各バリエーションは1バイトのメモリを占有し、以下のように
  • を推定する.
  • #[repr(u16)]
  • #[repr(u32)]
  • #[repr(u64)]
  • #[repr(i8)]
  • #[repr(i16)]
  • #[repr(i32)]
  • #[repr(i64)]

  • Cレイアウトの指定
  • #[repr(C)]

  • 現在のプラットフォームのCコンパイラによって決定されるCレイアウトを指定します.つまりRust側は相手側のCコンパイラの約束と一致(例えば4バイト)しており、プラットフォームによってはCコンパイラによって異なる場合があります.
    グループ指定
  • #[repr(C, u8)]
  • #[repr(C, u16)]

  • コンビネーション指定は、負荷付き列挙にのみ使用できます(ただし、負荷付き列挙は実際の場合、FFI境界にまたがるシーンは多くありません.必要であれば、後で説明します).
    Fieldless enumはintビット数レイアウトとCレイアウトのいずれかしか指定できず、組み合わせて指定することはできません.次のようになります.
    #[repr(C)]
    enum SomeEnum { 
      A, 
      B, 
      C, 
    }
    
    

    Cに変換すると、Aと整数を比較することができます(0から増加し、ここではA=0、B=1、C=2).その他の後続はCに列挙された知識であり、これは言うまでもありません.
    重要なリファレンス
    以下のリンクは、読む価値があります.
  • https://blog.eqrion.net/announcing-cbindgen/
  • https://s3.amazonaws.com/temp.michaelfbryan.com/objects/index.html
  • https://rust-lang.github.io/unsafe-code-guidelines/layout/enums.html