如何在编译为WebAssembly的Rust库中使用C库?

时间:2018-08-03 06:44:17

标签: javascript c rust webassembly

我正在尝试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中。

1 个答案:

答案 0 :(得分:11)

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说“链接子节过早结束”。 w,是的(顺便说一下,我的Rust设置也是全新的)。然后我读到有两个clang wasm目标:wasm32-unknown-unknown-wasmwasm32-unknown-unknown-elf,也许在这里应该使用后者。由于我的全新llvm+clang版本也与此目标发生内部错误,要求我将错误报告发送给开发人员,因此可能需要在简单或中等条件下进行测试,例如在某些* nix或Mac盒上进行测试

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

这时,我刚刚将lld添加到llvm并成功地从位代码文件中手动链接了测试代码:

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

是的,它对数字求和,C中为2,Rust中为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) }
}

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

侧面故事:字符串

由于alloc及其Layout令我有些害怕,我采用了不时描述/使用的基于向量的方法,例如here或{{3 }}。
这是一个示例,从外部获取“ 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都可能起作用(至少两次调用它会引发恐慌)。 br /> 途中有一个重要的教训:当模块管理其内存时,其大小可能会发生变化,从而导致备用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

我不得不在Rust工具链的深处使用lld,因为.rlib-s被称为Hello, Rust!,所以它们绑定到{{1} }工具链

  

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

当然,这个staticlib不会吃掉lld.wasm生成的.o / clang文件(“链接子节过早结束”) ,也许Rust部分也应该使用自定义llc进行重建。
另外,此构建似乎缺少实际的分配器,除了llvm之外,导入表中还会有4个条目:chello__rust_alloc__rust_alloc_zeroed和{{ 1}}。毕竟,实际上可以从JavaScript中提供它,只是打败了让Rust处理自己的内存的想法,而且在单遍__rust_dealloc构建中存在一个分配器...哦,是的,这就是我在这里放弃了本周(2018年8月11日,21:56)

新的一周,与Binaryen __rust_realloc的新冒险

这个想法是修改现成的Rust代码(具有分配器和所有组件)。而这一作品。只要您的C代码中没有数据。

概念证明代码:

chello.c

rustc

不太常见,但这是C代码。

wasm-dis/merge

({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; } 与“边的故事:字符串”中介绍的内容相同)
结果为

mhello.html

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从重复的有和没有<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),则结果将是短几个字节(这些字节位于正在运行的代码的中间,而不是位于e​​xport / import / data部分中)
  • 合并事项的顺序,带有“ Rust-origin”的文件必须先行。 .wasm死于娱乐性消息
      

    [模块中的Wasm-validator错误]意外的false:段偏移量应该合理,在
      [i32](i32.const 1)
      致命:验证输出时出错

  • 可能是我的错,但是我必须构建一个完整的wasm-as模块(因此,带有链接)。仅编译(wasm-merge chello.wast rhello.wast [...])导致可重定位的模块在本故事的开头就被遗漏了很多,但是反编译后,一个模块(到chello.wasm)丢失了指定的导出(clang -c [...]) :
    .wast完全消失
    chello()成为(export "chello" (func $chello)),是一个内部函数((func $chello ...丢失了(func $0 ...wasm-dis节,仅在汇编源中加入了有关它们及其大小的说明)< / li>
  • 与上一个有关:通过这种方式(构建完整的模块),辅助模块中的数据无法由reloc进行重定位:尽管有机会捕获对字符串本身的引用({{1} }尤其是在偏移量1024处成为常量,如果在函数内部是局部常量,则以后称为linking,则不会发生。如果它是一个全局常量,那么它的地址也将变成一个全局常量,将数字1024存储在偏移量1040处,并且该字符串将被称为wasm-merge,这开始变得难以捕获。

为了笑,该代码也可以编译和工作...

const char *HELLO="Hello from C";

...只是在Rust的消息池中间写了“ C的Hello”,导致打印输出

  

您好,来自Clt :: unwrap()`上有一个`Err`值和Rust!

(说明:由于优化标志(i32.const 1024),重新编译的代码中没有0初始化程序)
并且还提出了有关定位(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); } 的情况下进行定义,-O提到libcmy_作为内置对象,这也告诉我们正确的特性,它不会为它们发出代码,它们将成为结果模块的导入。