web-dev-qa-db-ja.com

WebAssemblyにコンパイルされたRustライブラリでCライブラリを使用するにはどうすればよいですか?

Rust、WebAssembly、Cの相互運用性を試し、最終的にはブラウザまたはNode.jsでRust(静的C依存関係あり)ライブラリを使用します。 wasm-bindgen JavaScriptグルーコード用。

#![feature(libc, use_extern_macros)]
extern crate wasm_bindgen;

use wasm_bindgen::prelude::*;
use std::os::raw::c_char;
use std::ffi::CStr;

extern "C" {
    fn hello() -> *const c_char; // returns "hello from C" 
}

#[wasm_bindgen]
pub fn greet() -> String {
    let c_msg = unsafe { CStr::from_ptr(hello()) };
    format!("{} and Rust!", c_msg.to_str().unwrap())
}

私の最初の素朴なアプローチはbuild.rs gccクレートを使用してCコードから静的ライブラリを生成するスクリプト。 WASMビットを導入する前に、Rustプログラムをコンパイルしてhello from Cコンソールでの出力、今コンパイラからのエラーが表示されます

Rust-lld: error: unknown file type: hello.o

build.rs

extern crate gcc;                                                                                         

fn main() {
    gcc::Build::new()
        .file("src/hello.c")
        .compile("libhello.a");
}

これは、hello.oファイルは、WebAssemblyではなく、私のラップトップのアーキテクチャ用にコンパイルされました。

理想的には、これをそのまま使用してbuild.rsに魔法を追加します。たとえば、Cライブラリをコンパイルして、Rustが使用できる静的WebAssemblyライブラリになるようにします。

うまくいくと思いますが、問題が多いように思われるので避けたいのですが、Emscriptenを使用してCコードのWASMライブラリを作成し、次にRustライブラリを個別にコンパイルして、 JavaScript。

26
olanod

TL; DR:「新しい週、新しい冒険」にジャンプして、「CとRustからこんにちは!」

良い方法は、WASMライブラリを作成し、それをリンカーに渡すことです。 rustcにはそのためのオプションがあります(ソースコードディレクティブもあるようです)。

_rustc <yourcode.rs> --target wasm32-unknown-unknown --crate-type=cdylib -C link-arg=<library.wasm>
_

トリックは、ライブラリはライブラリでなければならないということです。そのため、reloc(実際にはlinking)セクションを含める必要があります。 EmscriptenはそのためのシンボルRELOCATABLEを持っているようです:

_emcc <something.c> -s WASM=1 -s SIDE_MODULE=1 -s RELOCATABLE=1 -s EMULATED_FUNCTION_POINTERS=1 -s ONLY_MY_CODE=1 -o <something.wasm>
_

(_EMULATED_FUNCTION_POINTERS_はRELOCATABLEに含まれているため、実際には必要ありません。_ONLY_MY_CODE_はいくつかの余分なものを取り除きますが、ここでも関係ありません)

重要なのは、emccが再配置可能なwasmファイルを生成しなかったことです。少なくとも今週ダウンロードしたバージョンでは、Windows用ではありませんでした(私はハードの難易度でこれをプレイしました。最高のアイデア)。したがって、セクションが欠落しており、rustcは_<something.wasm> is not a relocatable wasm file_について不平を言い続けます。

次にclangが出てきます。これにより、非常にシンプルな1行で再配置可能なwasmモジュールを生成できます。

_clang -c <something.c> -o <something.wasm> --target=wasm32-unknown-unknown
_

次に、rustcは「リンクのサブセクションが途中で終了しました」と言います。ああ、はい(ちなみに、私のRustセットアップも新品でした)。次に、2つのclangwasmターゲットがあると読みました:_wasm32-unknown-unknown-wasm_と_wasm32-unknown-unknown-elf_、そしておそらく後者をここで使用する必要があります。新しい_llvm+clang_ビルドもこのターゲットで内部エラーが発生するため、開発者にエラーレポートを送信するように要求します。 * nixやMacのボックスなど、簡単または中程度でテストする必要があるかもしれません。

最小限の成功事例:3つの数値の合計

この時点で、lldllvmに追加し、ビットコードファイルから手動でテストコードをリンクすることに成功しました。

_clang cadd.c --target=wasm32-unknown-unknown -emit-llvm -c
rustc rsum.rs --target wasm32-unknown-unknown --crate-type=cdylib --emit llvm-bc
lld -flavor wasm rsum.bc cadd.bc -o msum.wasm --no-entry
_

ああ、それは数値を合計します。2はCに、1 + 2はRustにあります。

cadd.c

_int cadd(int x,int y){
  return x+y;
}
_

msum.rs

_extern "C" {
    fn cadd(x: i32, y: i32) -> i32;
}

#[no_mangle]
pub fn rsum(x: i32, y: i32, z: i32) -> i32 {
    x + unsafe { cadd(y, z) }
}
_

test.html

_<script>
  fetch('msum.wasm')
    .then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.compile(bytes))
    .then(module => {
      console.log(WebAssembly.Module.exports(module));
      console.log(WebAssembly.Module.imports(module));
      return WebAssembly.instantiate(module, {
        env:{
          _ZN4core9panicking5panic17hfbb77505dc622acdE:alert
        }
      });
    })
    .then(instance => {
      alert(instance.exports.rsum(13,14,15));
    });
</script>
_

その__ZN4core9panicking5panic17hfbb77505dc622acdE_は非常に自然に感じられ(モジュールは、エクスポートとインポートをログに記録するために2つのステップでコンパイルおよびインスタンス化されます。つまり、欠落している部分を見つける方法です)、この試みの終焉を予測します。ランタイムライブラリへの他の参照がなく、この特定のメソッドを手動でモック/提供できるため、すべてが機能します。

サイドストーリー:ひも

alloc とそのLayoutのことで少し怖がったので、たとえば here または Hello、Rust! に。
これは、「Hello from ...」という文字列を外部から取得する例です...

rhello.rs

_use std::ffi::CStr;
use std::mem;
use std::os::raw::{c_char, c_void};
use std::ptr;

extern "C" {
    fn chello() -> *mut c_char;
}

#[no_mangle]
pub fn alloc(size: usize) -> *mut c_void {
    let mut buf = Vec::with_capacity(size);
    let p = buf.as_mut_ptr();
    mem::forget(buf);
    p as *mut c_void
}

#[no_mangle]
pub fn dealloc(p: *mut c_void, size: usize) {
    unsafe {
        let _ = Vec::from_raw_parts(p, 0, size);
    }
}

#[no_mangle]
pub fn hello() -> *mut c_char {
    let phello = unsafe { chello() };
    let c_msg = unsafe { CStr::from_ptr(phello) };
    let message = format!("{} and Rust!", c_msg.to_str().unwrap());
    dealloc(phello as *mut c_void, c_msg.to_bytes().len() + 1);
    let bytes = message.as_bytes();
    let len = message.len();
    let p = alloc(len + 1) as *mut u8;
    unsafe {
        for i in 0..len as isize {
            ptr::write(p.offset(i), bytes[i as usize]);
        }
        ptr::write(p.offset(len as isize), 0);
    }
    p as *mut c_char
}
_

_rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib_として構築

...そして実際にJavaScriptを操作する:

jhello.html

_<script>
  var e;
  fetch('rhello.wasm')
    .then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.compile(bytes))
    .then(module => {
      console.log(WebAssembly.Module.exports(module));
      console.log(WebAssembly.Module.imports(module));
      return WebAssembly.instantiate(module, {
        env:{
          chello:function(){
            var s="Hello from JavaScript";
            var p=e.alloc(s.length+1);
            var m=new Uint8Array(e.memory.buffer);
            for(var i=0;i<s.length;i++)
              m[p+i]=s.charCodeAt(i);
            m[s.length]=0;
            return p;
          }
        }
      });
    })
    .then(instance => {
      /*var*/ e=instance.exports;
      var ptr=e.hello();
      var optr=ptr;
      var m=new Uint8Array(e.memory.buffer);
      var s="";
      while(m[ptr]!=0)
        s+=String.fromCharCode(m[ptr++]);
      e.dealloc(optr,s.length+1);
      console.log(s);
    });
</script>
_

特に美しくはありませんが(実際にはRustについての手掛かりはありません)、期待どおりの結果が得られ、deallocでも機能する可能性があります(少なくとも2回呼び出すとパニックが発生します)。
途中で重要な教訓がありました:モジュールがそのメモリを管理するとき、そのサイズが変わる可能性があり、その結果、バッキングArrayBufferオブジェクトとそのビューが無効になります。そのため、_memory.buffer_が複数回チェックされ、wasmコードを呼び出してafterがチェックされます。

そして、このコードはランタイムライブラリと_.rlib_- sを参照するので、これは私が行き詰まっているところです。手動ビルドに最も近いものは次のとおりです。

_rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib --emit obj
lld -flavor wasm rhello.o -o rhello.wasm --no-entry --allow-undefined
     liballoc-5235bf36189564a3.rlib liballoc_system-f0b9538845741d3e.rlib
     libcompiler_builtins-874d313336916306.rlib libcore-5725e7f9b84bd931.rlib
     libdlmalloc-fffd4efad67b62a4.rlib liblibc-453d825a151d7dec.rlib
     libpanic_abort-43290913ef2070ae.rlib libstd-dcc98be97614a8b6.rlib
     libunwind-8cd3b0417a81fb26.rlib
_

Rustツールチェーンの_.rlib_- sは interpreted と言われているため、私がlldを使用する必要があった場所Rustツールチェーンにバインドされている

_--crate-type=rlib_、_#[crate_type = "rlib"]_-「Rustライブラリ」ファイルが作成されます。これは中間アーティファクトとして使用され、「静的Rustライブラリ」と見なすことができます。これらのrlibファイルは、staticlibファイルとは異なり、 Rustコンパイラは将来のリンケージに含まれます。これは基本的に、rustcが動的ライブラリでメタデータを探すようにrlibファイルでメタデータを探すことを意味します。この形式の出力は、静的にリンクされた実行可能ファイルとstaticlib出力を生成するために使用されます。

もちろん、このlldは、clangまたはllcで生成された_.wasm_/_.o_ファイルを食べません(「リンクサブセクションが途中で終了しました」) 、おそらくRustパーツもカスタムllvmで再構築する必要があります。
また、このビルドにはchello以外に実際のアロケータが欠落しているようです。インポートテーブルには、___Rust_alloc_、___Rust_alloc_zeroed_、___Rust_dealloc_および___Rust_realloc_。結局のところ、これは実際にはJavaScriptから提供できますが、Rustに独自のメモリを処理させるという考えに打ち勝ち、さらに、単一パスrustcビルドにアロケーターが存在していました。 ..ああ、はい、ここで今週は諦めました(2018年8月11日21:56)

新しい週、新しい冒険、Binaryen、_wasm-dis/merge_

アイデアは、既製のRustコード(アロケータとすべてが適切に配置されている)を変更することです。そして、これは機能します。Cコードにデータがない場合に限ります。

概念実証コード:

chello.c

_void *alloc(int len); // allocator comes from Rust

char *chello(){
  char *hell=alloc(13);
  hell[0]='H';
  hell[1]='e';
  hell[2]='l';
  hell[3]='l';
  hell[4]='o';
  hell[5]=' ';
  hell[6]='f';
  hell[7]='r';
  hell[8]='o';
  hell[9]='m';
  hell[10]=' ';
  hell[11]='C';
  hell[12]=0;
  return hell;
}
_

それほど普通ではありませんが、Cコードです。

_rustc rhello.rs --target wasm32-unknown-unknown --crate-type=cdylib
wasm-dis rhello.wasm -o rhello.wast
clang chello.c --target=wasm32-unknown-unknown -nostdlib -Wl,--no-entry,--export=chello,--allow-undefined
wasm-dis a.out -o chello.wast
wasm-merge rhello.wast chello.wast -o mhello.wasm -O
_

(_rhello.rs_は、「サイドストーリー:文字列」で示したものと同じです)
そして結果は

mhello.html

_<script>
  fetch('mhello.wasm')
    .then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.compile(bytes))
    .then(module => {
      console.log(WebAssembly.Module.exports(module));
      console.log(WebAssembly.Module.imports(module));
      return WebAssembly.instantiate(module, {
        env:{
          memoryBase: 0,
          tableBase: 0
        }
      });
    })
    .then(instance => {
      var e=instance.exports;
      var ptr=e.hello();
      console.log(ptr);
      var optr=ptr;
      var m=new Uint8Array(e.memory.buffer);
      var s="";
      while(m[ptr]!=0)
        s+=String.fromCharCode(m[ptr++]);
      e.dealloc(optr,s.length+1);
      console.log(s);
    });
</script>
_

アロケータでさえ何かをしているようです(ptrの有無にかかわらず繰り返されるブロックからのdeallocの読み取りは、メモリがどのようにリーク/リークしないかを示しています)。

もちろん、これは非常に壊れやすく、神秘的な部分もあります。

  • 最終的なマージが_-S_スイッチ(_.wasm_ではなくソースコードを生成)で実行され、結果のアセンブリファイルが個別にコンパイルされる場合(_wasm-as_を使用)、結果は数バイトになります。より短い(そしてそれらのバイトは、エクスポート/インポート/データセクションではなく、実行中のコードの真ん中のどこかにあります)
  • マージ事項の順序は、「Rust-Origin」でファイルすることを最初にする必要があります。 _wasm-merge chello.wast rhello.wast [...]_が楽しいメッセージで死ぬ

    [モジュールのwasm-validatorエラー]予期しないfalse:セグメントのオフセットは妥当なはずです
    [i32](i32.const 1)
    致命的:出力の検証中にエラーが発生しました

  • おそらく私のせいですが、完全な_chello.wasm_モジュールを(リンク付きで)構築する必要がありました。 (_clang -c [...]_)のみをコンパイルすると、再配置可能なモジュールが発生しましたが、このストーリーの冒頭で非常に見落とされていましたが、(_.wast_へ)コンパイルすると、名前付きエクスポート(chello())が失われました):
    _(export "chello" (func $chello))_が完全に消えます
    _(func $chello ..._は_(func $0 ..._になり、内部関数(_wasm-dis_はrelocおよびlinkingセクションを失い、それらとそのアセンブリソースへのサイズ)
  • 前のものに関連する:この方法(完全なモジュールの構築)で、セカンダリモジュールからのデータは_wasm-merge_によって再配置できません:文字列自体への参照をキャッチする可能性があります(_const char *HELLO="Hello from C";_は特にオフセット1024の定数で、後で_(i32.const 1024)_と呼ばれます(関数内のローカル定数の場合)。これは発生しません。そして、それがグローバル定数である場合、そのアドレスもグローバル定数となり、オフセット1040に格納される数値1024となり、文字列は_(i32.load offset=1040 [...]_として参照され、キャッチが困難になります。

笑ってみると、このコードはコンパイルされて動作します...

_void *alloc(int len);

int my_strlen(const char *ptr){
  int ret=0;
  while(*ptr++)ret++;
  return ret;
}

char *my_strcpy(char *dst,const char *src){
  char *ret=dst;
  while(*src)*dst++=*src++;
  *dst=0;
  return ret;
}

char *chello(){
  const char *HELLO="Hello from C";
  char *hell=alloc(my_strlen(HELLO)+1);
  return my_strcpy(hell,HELLO);
}
_

... Rustのメッセージプールの真ん中に「Hello from C」と書き込むだけで、結果として出力されます。

こんにちは、Clt :: unwrap() `から` Err`値とRustを受け取ります!

(説明:最適化フラグ_-O_が原因で、0-初期化子は再コンパイルされたコードに存在しません)
また、libcの検索に関する質問も表示されます(ただし、_my__なしでそれらを定義すると、clangは、strlenstrcpyビルトインとして、正しいシンガチャーも通知し、コードを出力せず、結果のモジュールのインポートになります)。

14
tevemadar