概述
在当今的 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 方面的支持得到了特别的关注,并推出了专门的工具链和库:
- wasm-bindgen:用于将 Rust 代码与 JavaScript 进行互操作,提供了方便的工具来生成 WebAssembly 和 JavaScript 桥接代码。
- wasm-pack:一种集成工具,用于打包 Rust WebAssembly 项目并发布到 npm。
- stdweb 和 yew:提供类似于 React 的前端框架,帮助开发者使用 Rust 构建前端 Web 应用。
Emscripten
Emscripten 是一个开放源代码的编译工具链,它能够将 C 和 C++ 代码编译成 WebAssembly(.wasm)或 asm.js,以便在现代 Web 浏览器上运行。它基于 LLVM 编译器基础设施,为开发人员提供了一种将桌面应用程序迁移到 Web 的方式,同时还保持了跨平台的兼容性。
正文
EMCC官网安装
本文以此种方式安装
按照上面的官方文档一步步实现即, 本文版本选择的是 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"");
}
编译选项说明
-
EXPORTED_RUNTIME_METHODSlet 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函数。
-
EXPORT_NAME和MODULARIZEprintln!("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 模块。
- 作用:
-
ALLOW_MEMORY_GROWTHprintln!("cargo:rustc-link-arg=-s"); println!("cargo:rustc-link-arg=ALLOW_MEMORY_GROWTH=1");- 作用:允许 WebAssembly 内存在运行时动态增长,以适应更大的内存需求。
-
IMPORTED_MEMORYprintln!("cargo:rustc-link-arg=-s"); println!("cargo:rustc-link-arg=IMPORTED_MEMORY=1");- 作用:允许 WebAssembly 模块从 JavaScript 中导入并共享内存,而不是自定义创建新的内存对象。这有助于在多个模块之间共享内存。
-
MALLOCprintln!("cargo:rustc-link-arg=-s"); println!("cargo:rustc-link-arg=MALLOC="emmalloc"");- 作用:指定 Emscripten 使用
emmalloc作为分配器,以减少内存使用。 - 解释:
emmalloc:一种更小更快的内存分配器,适用于低内存使用的场景。
- 作用:指定 Emscripten 使用
更多的指令可以详细阅读 # 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']);
- 第一个参数: 方法签名
- 第二个参数: 返回类型
- 第三个参数: 入参类型
- 第四个参数: 入参值