首页 > 解决方案 > 如何在编译为 WebAssembly 的 Rust 库中使用 C 库?

问题描述

我正在试验 Rust、WebAssembly 和 C 互操作性,以最终在浏览器或 Node.js 中使用 Rust(具有静态 C 依赖项)库。我正在使用wasm-bindgenJavaScript 胶水代码。

#![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 crate 从 C 代码生成一个静态库。在引入 WASM 位之前,我可以编译 Rust 程序并hello from C在控制台中查看输出,现在我从编译器得到一个错误说

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

构建.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 中将它们粘合在一起。

标签: javascriptcrustwebassembly

解决方案


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代码生成一个可重定位的模块:wasm

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

然后rustc说“链接子部分过早结束”。哦,是的(顺便说一下,我的 Rust 设置也是全新的)。然后我读到有两个clang wasm目标:wasm32-unknown-unknown-wasmand wasm32-unknown-unknown-elf,也许后一个应该在这里使用。由于我的全新llvm+clang版本在此目标上遇到了内部错误,要求我向开发人员发送错误报告,这可能是在简单或中等上测试的东西,比如在某些 *nix 或 Mac 机器上。

最小的成功案例:三个数字的总和

此时我刚刚添加并成功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

哦,是的,它对数字求和,CRust 中的 2 和 1+2:

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) }
}

测试.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感觉很自然(模块分两步编译和实例化,以便记录导出和导入,这是找到这些缺失部分的一种方式),并预示着这种尝试的失败:整个事情都有效,因为有没有其他对运行时库的引用,并且可以手动模拟/提供此特定方法。

支线故事:弦

由于allocLayout的事情让我有点害怕,我不时使用描述/使用的基于矢量的方法,例如在这里或在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也可能奏效(至少调用它两次会引发恐慌)。
在此过程中有一个重要的教训:当模块管理其内存时,它的大小可能会发生变化,从而导致支持ArrayBuffer对象及其视图无效。这就是为什么memory.buffer要多次检查,并调用wasm代码后检查的原因。

这就是我卡住的地方,因为这段代码将引用运行时库和.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

我不得不使用lld坐在 Rust 工具链深处的地方,因为.rlib-s 据说是被解释的,所以它们绑定到Rust工具链

--crate-type=rlib, #[crate_type = "rlib"]- 将生成一个“Rust 库”文件。这被用作中间工件,可以被认为是“静态 Rust 库”。这些rlib文件与staticlib文件不同,在未来的链接中由 Rust 编译器解释。这实质上意味着rustc它将在rlib文件中查找元数据,就像在动态库中查找元数据一样。这种形式的输出用于生成静态链接的可执行文件以及staticlib输出。

当然这lld不会吃掉用or生成的.wasm/.o文件(“链接子部分过早结束”),也许 Rust 部分也应该用 custom 重建。 此外,此构建似乎缺少实际的分配器,此外,导入表中还会有 4 个条目:、、和。毕竟这实际上可以由 JavaScript 提供,只是破坏了让 Rust 处理自己的内存的想法,而且在单通道构建中存在分配器......哦,是的,这就是我本周放弃的地方(2018 年 8 月 11 日 21:56)clangllcllvm
chello__rust_alloc__rust_alloc_zeroed__rust_dealloc__rust_reallocrustc

新的一周,新的冒险,与 Binaryen,wasm-dis/merge

这个想法是修改现成的 Rust 代码(分配器和一切就绪)。这个有效。只要您的 C 代码没有数据。

概念验证代码:

hello.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显示内存如何不会相应地泄漏/泄漏)。

当然,这是超级脆弱的,也有神秘的部分:

  • 如果最终合并使用-Sswitch 运行(生成源代码而不是.wasm),并且结果程序集文件单独编译(使用wasm-as),则结果将短几个字节(并且这些字节位于运行代码的中间位置,不在导出/导入/数据部分)
  • 合并的顺序很重要,带有“Rust-origin”的文件必须放在第一位。wasm-merge chello.wast rhello.wast [...]死于有趣的消息

    [wasm-validator error in module] 意外错误:段偏移应该是合理的,在
    [i32] (i32.const 1)上
    致命:验证输出时出错

  • 可能是我的错,但我必须构建一个完整的chello.wasm模块(因此,通过链接)。仅编译 ( clang -c [...]) 导致了可重定位模块,该模块在本文一开始就被遗漏了很多,但是反编译一个 (to .wast) 丢失了命名的导出 ( chello()):
    (export "chello" (func $chello))完全消失
    (func $chello ...变成(func $0 ...一个内部函数 (wasm-dis丢失reloclinking部分,只在汇编源代码中对它们及其大小进行注释)
  • 与前一个相关:这种方式(构建一个完整的模块)来自辅助模块的数据不能被重新定位wasm-merge:虽然有机会捕获对字符串本身的引用(const char *HELLO="Hello from C";特别是在偏移量 1024 处变为常量,稍后引用好像(i32.const 1024)它是局部常量,在函数内部),它不会发生。如果它是一个全局常量,它的地址也变成了一个全局常量,数字 1024 存储在偏移量 1040 处,字符串将被称为(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`an 值和 Rust!

(解释:由于优化标志,重新编译的代码中不存在 0-initializers,-O
它还提出了关于定位 a 的问题libc(尽管将它们定义为没有my_,clang提及strlenstrcpy作为内置插件,也告诉他们正确的签名,它不会为它们发出代码,它们会成为结果模块的导入)。


推荐阅读