Rust + Emscripten 构建高效与安全的 WebAssembly 应用:桥接 JavaScript 与 C/C++ 库

607 阅读5分钟

DALL·E 2024-05-09 17.12.05 - A detailed illustration showing the concept of Rust and Emscripten compiling to WebAssembly (.wasm) and bridging with JavaScript (.js). The image shou.webp

概述

在当今的 Web 开发环境中,浏览器的 F12 开发者工具总能让用户轻松查看到 JavaScript 相关的逻辑处理代码。尽管这对开发和调试来说很有用,但也容易引发代码被逆向工程或被窃取的问题。

WebAssembly(Wasm)是一种在浏览器中运行的低级编程语言,拥有更高的执行速度和安全性。然而,尽管 wasm-bindgen 提供了方便的工具,用于将 Rust 代码与 JavaScript 进行互操作,但它在与功能更强大的 C/C++ 库交互方面却存在局限性。特别是当我们希望利用像 OpenSSL 这类底层由 C++ 实现的强大库时,我们希望上层能充分利用 Rust 的强大特性来实现更丰富的功能。

因此,本文将探讨如何利用 Rust 和 Emscripten 来实现 WebAssembly 代码编译和 JavaScript 桥接,使我们能够:

充分发挥 Rust 在 WebAssembly 方面的优势,提供更安全、高效的代码。 使用 Emscripten 实现 Rust 与 C/C++ 库的桥接,允许我们无缝使用像 OpenSSL 这样的底层库。 利用 JavaScript 与 .wasm 桥接,让我们能够在 Web 环境中尽享底层库的强大功能。 接下来,我们将探讨具体的技术实现方案,并给出如何在 Web 上使用 Rust、Emscripten、.wasm 和 JavaScript 来构建高效、安全的应用程序的示例。

Rust

这里不展开了,相信看这篇文章的都是有Rust基础的

Rust 与 WebAssembly

Rust 在 WebAssembly 方面的支持得到了特别的关注,并推出了专门的工具链和库:

  1. wasm-bindgen:用于将 Rust 代码与 JavaScript 进行互操作,提供了方便的工具来生成 WebAssembly 和 JavaScript 桥接代码。
  2. wasm-pack:一种集成工具,用于打包 Rust WebAssembly 项目并发布到 npm。
  3. stdwebyew:提供类似于 React 的前端框架,帮助开发者使用 Rust 构建前端 Web 应用。

Emscripten

Emscripten 是一个开放源代码的编译工具链,它能够将 C 和 C++ 代码编译成 WebAssembly(.wasm)或 asm.js,以便在现代 Web 浏览器上运行。它基于 LLVM 编译器基础设施,为开发人员提供了一种将桌面应用程序迁移到 Web 的方式,同时还保持了跨平台的兼容性。

正文

EMCC官网安装

本文以此种方式安装

# Emscripten SDK (emsdk)

按照上面的官方文档一步步实现即, 本文版本选择的是 3.1.14

最后配置环境 ~/.zshrc 中添加以下内容

source ~/Library/emsdk/emsdk_env.sh

Homebrew安装EMCC

当然MacOS 上也可以使用 brew 进行安装

brew install emscripten

Rust项目

创建 Build.rs

├── build.rs
├── src
│    ├── lib.rs
├── Cargo.toml
// build.rs
fn main() {
    println!("cargo:rustc-link-arg=-s");
    let mut method_vec = vec![];
    method_vec.push("ccall");
    method_vec.push("cwrap");
    method_vec.push("callMain");
    println!("cargo:rustc-link-arg=EXPORTED_RUNTIME_METHODS={:?}", method_vec);

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=EXPORT_NAME="HelloModule"");
    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=MODULARIZE=1");

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=ALLOW_MEMORY_GROWTH=1");

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=IMPORTED_MEMORY=1");

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=MALLOC="emmalloc"");
}

编译选项说明

  1. EXPORTED_RUNTIME_METHODS

    let mut method_vec = vec![];
    method_vec.push("ccall");
    method_vec.push("cwrap");
    method_vec.push("callMain");
    println!("cargo:rustc-link-arg=EXPORTED_RUNTIME_METHODS={:?}", method_vec);
    
    • 作用:告诉 Emscripten 哪些运行时方法应包含在生成的 JavaScript 文件中。
    • 各方法说明
      • ccall:用于从 JavaScript 调用 WebAssembly 函数的通用接口。
      • cwrap:封装 ccall 的包装器,返回一个可直接调用的 JavaScript 函数。
      • callMain:用于调用 WebAssembly 模块中的 main 函数。
  2. EXPORT_NAMEMODULARIZE

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=EXPORT_NAME="HelloModule"");
    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=MODULARIZE=1");
    
    • 作用
      • EXPORT_NAME:设置生成的模块名称,以便在 JavaScript 中以特定名称引用 WebAssembly模块。
      • MODULARIZE:生成的 JavaScript 文件会返回一个函数,该函数用于异步加载和初始化 WebAssembly 模块。
  3. ALLOW_MEMORY_GROWTH

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=ALLOW_MEMORY_GROWTH=1");
    
    • 作用:允许 WebAssembly 内存在运行时动态增长,以适应更大的内存需求。
  4. IMPORTED_MEMORY

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=IMPORTED_MEMORY=1");
    
    • 作用:允许 WebAssembly 模块从 JavaScript 中导入并共享内存,而不是自定义创建新的内存对象。这有助于在多个模块之间共享内存。
  5. MALLOC

    println!("cargo:rustc-link-arg=-s");
    println!("cargo:rustc-link-arg=MALLOC="emmalloc"");
    
    • 作用:指定 Emscripten 使用 emmalloc 作为分配器,以减少内存使用。
    • 解释
      • emmalloc:一种更小更快的内存分配器,适用于低内存使用的场景。

更多的指令可以详细阅读 # Emscripten SDK (emsdk)

关于编译

rust项目中添加emcc工具链

rustup target add wasm32-unknown-emscripten

重点

  • AR 是一个用于创建静态库的工具,它将多个目标文件(.o 文件)打包成一个静态库文件(.a 文件)。在使用 Emscripten 的上下文中,llvm-ar 是 LLVM 版本的 AR 工具,专门用于处理与 LLVM 兼容的目标文件和静态库。
  • RANLIB 是一个用于生成和优化静态库索引的工具。静态库文件(.a 文件)包含了很多目标文件,而这些文件在库中是随机访问的。ranlib 生成一个目录,加速链接器搜索静态库中的符号,提高链接速度。在 Emscripten 环境下使用 llvm-ranlib,确保静态库与 LLVM 工具链的其他部分兼容。 这里要加入临时的环境变量
AR=~/Library/emsdk/upstream/bin/llvm-ar \
RANLIB=~/Library/emsdk/upstream/bin/llvm-ranlib \
cargo build --all --target wasm32-unknown-emscripten --release

如果你不加入这两个交叉编译的环境, 在编译的时候是会报错的

生成产物

├── target 
│     ├── wasm32-unknown-emscripten
│     │      ├── release
│     │      │      ├── hellowasm-rust.js
│     │      │      ├── hellowasm_rust.wasm
  • hellowasm_rust.wasm生成的二进制文件
  • hellowasm-rust.js生成的FFI文件

Example

Rust代码

#[no_mangle]
pub unsafe extern "C" fn j2w_hello(hello: *const c_char) -> *mut c_char {
    let hello = CStr::from_ptr(hello).to_str().expect("hello is not a valid string");
    CString::new(format!("{hello} world")).unwrap().into_raw()
}

js代码

const wasmBytes = fileToBytes() 
const module = await HelloModule({ 
    // 将.wasm字节数组传入 WebAssembly 模块
    wasmBinary: wasmBytes, 
    // 初始内存大小
    INITIAL_MEMORY: 16 * 1024 * 1024, 
}); 
const res = module.ccall('j2w_hello', 'string', ['string'], ['hello']);
console.info(res)
// print: hello world

关于module.ccall('j2w_hello', 'string', ['string'], ['hello']);

  • 第一个参数: 方法签名
  • 第二个参数: 返回类型
  • 第三个参数: 入参类型
  • 第四个参数: 入参值

撰写不易,请给个赞👍吧