如何在编译为 WebAssembly 的 Rust 库中使用 C 库?
Posted
技术标签:
【中文标题】如何在编译为 WebAssembly 的 Rust 库中使用 C 库?【英文标题】:How do I use a C library in a Rust library compiled to WebAssembly? 【发布时间】:2019-01-10 23:59:21 【问题描述】:我正在试验 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 crate 从 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 中将它们粘合在一起。
【问题讨论】:
使用 Emscripten 是唯一的方法,因为 Emscripten 是一种将任意 C 代码(LLVM 可以支持的任何东西)编译为 WebAssembly 的工具。但是,我懒得想办法再次安装和配置 Emscripten。 当然,但我认为也许只有 rust 工具可以很好地集成到构建脚本中。或者它看起来也像是 wasm-bindgen 项目最终可以做的事情?比如将C库编译成wsam、rust库然后生成链接它们的JS胶水代码? 我也有一个评论:从 JavaScript 粘合可能会很棘手,因为必须在调用之间来回分配+复制内存 blob(如果两个模块有自己的内存),或者他们需要精心设计对共享的导入内存的访问(因此它不会因调整大小或其他方式而失效),辅助模块根本不应该初始化它(如果没有进一步的魔法,它的数据部分肯定会与主模块发生冲突)。 惰性版本:使用 corrode 将 C lib 转换为 Rust,然后将其粘贴到您的 crate 中。 【参考方案1】:TL;DR:跳转到“新的一周,新的冒险”以获得“Hello from C and 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-wasm
和wasm32-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 或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
我不得不使用位于 Rust 工具链深处的 lld
,因为 .rlib
-s 据说是 interpreted,所以它们绑定到 Rust
工具链
--crate-type=rlib
,#[crate_type = "rlib"]
- 将生成一个“Rust 库”文件。这被用作中间工件,可以被认为是“静态 Rust 库”。这些rlib
文件与staticlib
文件不同,在未来的链接中由Rust 编译器解释。这实质上意味着rustc
将在rlib
文件中查找元数据,就像在动态库中查找元数据一样。这种形式的输出用于生成静态链接的可执行文件以及staticlib
输出。
当然,这个lld
不会吃.wasm
/.o
生成的clang
或llc
文件(“链接子部分过早结束”),也许锈部分也应该重建使用自定义 llvm
。
此外,这个构建似乎缺少实际的分配器,除了chello
,导入表中还有4个条目:__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 error in module] 意外错误:段偏移应该是合理的,on [i32] (i32.const 1) 致命:验证输出时出错
chello.wasm
模块(因此,需要链接)。仅编译 (clang -c [...]
) 导致了可重定位模块,该模块在本文一开始就被遗漏了很多,但是反编译该模块(到.wast
)丢失了命名导出(chello()
):@987654404 @ 完全消失(func $chello ...
变成 (func $0 ...
,一个内部函数(wasm-dis
丢失了 reloc
和 linking
部分,只在汇编源代码中添加了关于它们及其大小的注释)
与上一个相关:这种方式(构建一个完整的模块)来自辅助模块的数据不能被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
)
它还提出了关于定位libc
的问题(尽管在没有my_
、clang
的情况下定义它们时提到strlen
和strcpy
作为内置插件,也告诉它们正确的签名,它不会发出代码它们并成为结果模块的导入)。
【讨论】:
旁查是否有人关注:clang --target=wasm32-unknown-unknown-elf [...]
在 Linux 上也因内部编译器错误而死。以上是关于如何在编译为 WebAssembly 的 Rust 库中使用 C 库?的主要内容,如果未能解决你的问题,请参考以下文章
Rust + Go 双剑合璧:WebAssembly 领域应用
编译为 Wasm 时,指向堆分配内存的 Rust 指针可以为 0 吗?