ToB企服应用市场:ToB评测及商务社交产业平台

标题: WebAssembly实践指南——C++和Rust通过wasmtime实现相互调用实例 [打印本页]

作者: 勿忘初心做自己    时间: 2023-9-24 15:52
标题: WebAssembly实践指南——C++和Rust通过wasmtime实现相互调用实例
C++和Rust通过wasmtime实现相互调用实例

1 wasmtime介绍

wasmtime是一个可以运行WebAssembly代码的运行时环境。
WebAssembly是一种可移植的二进制指令集格式,其本身与平台无关,类似于Java的class文件字节码。
WebAssembly本来的设计初衷是想让浏览器可以运行C语言这种编译型语言的代码。通常我们的C语言代码会使用gcc或clang等编译器直接编译链接成与平台相关的二进制可执行文件,这种与平台相关的二进制文件浏览器是无法直接运行的。如果想让浏览器运行C语言代码,就需要使用可将C语言编译成WebAssembly指令的编译器,编译好的代码是wasm格式。然后就可以使用各种wasm运行时来执行wasm代码,这就类似于JVM虚拟机执行class文件。
由于指令集和运行时环境本身与web场景并不绑定,因此随着后来的发展,WebAssembly指令集出现了可以脱离浏览器的独立运行时环境,WebAssembly的用途也变得更加广泛。
相比于浏览器的运行时,wasmtime是一个独立运行时环境,它可以脱离Web环境来执行wasm代码。它本身提供了命令行工具和API两种方式来执行wasm代码。本文主要介绍如何使用API方式来运行wasm代码。
2 wasmtime安装

2.1 wasmtime-cli安装

wasmtime-cli包含wasmtime命令,可以让我们直接在shell中运行wasm格式的代码。我们这里安装wasmtime主要是为了测试方便。
2.2 wasmtime库安装

如果想在代码中加载wasm文件并运行其中的代码,我们需要为我们使用的语言安装wasmtime库。注意这里的wasmtime库是为了让我们从代码中能够加载wasm文件并在wasmtime运行时中运行。wasmtime并不是wasm编译器,不能将C++或Rust代码编译成wasm文件,如果我们想将其他语言编译成wasm代码,需要下载各个语言自己的wasm编译器,具体安装方式在本文第3节。
目前wasmtime支持的语言有:
我们这里以Rust和C++为例介绍如何安装wasmtime库
Rust

在Rust中使用wasmtime库非常简单,我们只需要在Cargo.toml配置文件中添加如下依赖
  1. [dependencies]
  2. wasmtime = "12.0.2"
复制代码
C++

wasmtime的C++库需要我们引入wasmtime-cpp这个项目,wasmtime-cpp依赖wasmtime的C API,因此需要先安装C API。
3 wasm编译器安装

Rust

安装

Rust语言的编译器目前其实是一个LLVM的编译前端,它将代码编译成LLVM IR,然后经过LLVM编译成相应的目标平台代码。
因此我们并不需要替换Rust语言本身的编译器,只需要在编译时设置目标平台为wasm即可。我们在安装rust时,通常只会安装本机平台支持的目标,因此我们需要先安装wasm目标。
  1. # 列出所有可安装的target列表
  2. rustup target list
复制代码
使用上面的命令后可以看到很多可以安装的target列表,其中已经安装的target后面会有(installed)标示。注意到其中有3个wasm相关的target。
  1. wasm32-unknown-emscripten
  2. wasm32-unknown-unknown
  3. wasm32-wasi
复制代码
由于我们不需要在Web环境中运行Rust代码,因此我们选择安装wasm32-unknown-unknown和wasm32-wasi两个目标。运行以下两条指令,将这两个目标平台加入到当前使用的Rust工具链中。
  1. rustup target add wasm32-unknown-unknown
  2. rustup target add wasm32-wasi
复制代码
使用

当我们需要将一个Rust项目编译成wasm时,可以选择执行如下的两种编译命令
  1. # 在项目根目录执行
  2. cargo build --target wasm32-unknown-unknown  # 将在target/wasm32-unknown-unknown目录中生成build中间结果和wasm文件
  3. # 或者执行
  4. cargo build --target wasm32-wasi  # 将在target/wasm32-wasi目录中生成build中间结果和wasm文件
复制代码
C++

安装

目前,要将C++项目编译成WebAssembly,最常用的工具链是emscripten。emscripten支持将C,C++或任何使用了LLVM的语言编译成浏览器,Node.js或wasm运行时可以运行的代码。
Emscripten is a complete compiler toolchain to WebAssembly, using LLVM, with a special focus on speed, size, and the Web platform.
WebAssembly目前支持两种标准API:
Emscripten对JavaScript API做了重构,将其包装在与WASI接口一样的API中,然后Emscripten在编译代码时,将尽可能的使用WASI APIs,以此来避免不必要的API差异。因此Emscripten编译出来的wasm文件大部分时候可以同时运行在Web和非Web环境中。
使用如下命令下载emsdk
  1. git clone https://github.com/emscripten-core/emsdk.git
  2. cd emsdk
复制代码
使用如下命令安装最新的工具
  1. git pull
  2. ./emsdk install latest
  3. ./emsdk activate latest
复制代码
如果临时将emsdk的工具目录加入环境变量,可以运行
  1. source ./emsdk_env.sh
复制代码
或者可以在/etc/profile.d目录中创建emsdk.sh文件,并加入如下环境变量的配置,需要将替换为emsdk所在的目录。
  1. export PATH=$PATH:<emsdk_installed_dir>/emsdk:<emsdk_installed_dir>/emsdk/node/16.20.0_64bit/bin:<emsdk_installed_dir>/emsdk/upstream/emscripten
  2. export EMSDK=<emsdk_installed_dir>/emsdk
  3. export EMSDK_NODE=<emsdk_installed_dir>/emsdk/node/16.20.0_64bit/bin/node
复制代码
使用如下命令测试是否安装成功,如果输出下面的信息,说明我们已经可以正常使用emscripten的工具链。
  1. > emcc -v
  2. emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.45 (ef3e4e3b044de98e1811546e0bc605c65d3412f4)
  3. clang version 18.0.0 (https://github.com/llvm/llvm-project d1e685df45dc5944b43d2547d0138cd4a3ee4efe)
  4. Target: wasm32-unknown-emscripten
  5. Thread model: posix
  6. InstalledDir: <emsdk_installed_dir>/emsdk/upstream/bin
复制代码
使用

由于我们不使用Web运行时,下面将只介绍将C或C++代码编译成独立wasm二进制文件的使用方法。
  1. emcc -O3 hello.cpp -o hello.wasm
复制代码
当我们将输出目标的后缀名指定为wasm时,编译器会自动帮我们设置如下连接选项,上面的命令与下面的命令时等价的
  1. emcc -O3 hello.cpp -o hello.wasm -s STANDALONE_WASM
复制代码
这样编译出来的结果不会包含js文件,只会包含一个可被wasmtime运行的wasm文件。
更常用的方式通常是将整个C++项目编译成wasm,因此我们需要将工具链与cmake结合来构建整个项目。
假设我们有一个cmake项目有如下项目结构
  1. hello_project
  2.    |-hello.cpp
  3.    |-CMakeLists.txt
复制代码
其中hello.cpp中有如下代码
  1. #include <wasmtime.hh>int main() {  printf("hello, world!\n");  return 0;}
复制代码
CMakeLists.txt应该按照下面的方式进行改写
  1. cmake_minimum_required(VERSION 3.26)
  2. project(hello_project)
  3. add_definitions(-std=c++17)
  4. set(CMAKE_CXX_STANDARD 17)
  5. if (DEFINED EMSCRIPTEN)
  6.     add_executable(hello hello.cpp)
  7.     set(CMAKE_EXECUTABLE_SUFFIX  ".wasm")
  8.     set_target_properties(foo PROPERTIES COMPILE_FLAGS "-Os")
  9.     set_target_properties(foo PROPERTIES LINK_FLAGS "-Os -s WASM=1 -s STANDALONE_WASM")
  10. else()
  11.     add_executable(hello hello.cpp)
  12. endif ()
复制代码
以上CMakeLists.txt表示,当我们使用emscripten工具链进行编译时,将输出.wasm文件,且添加对应的编译和连接选项。当我们使用其他工具链编译时,将直接输出对应平台的可执行文件。
按照上面的方式写好CMakeLists.txt后,需要使用以下命令来执行编译的过程
  1. # 在项目根目录下
  2. mkdir build
  3. cd build
  4. # 执行emcmake命令会帮我们自动配置cmake中指定的工具链为emscripten的工具链,这样就确保了使用的编译工具为emcc或em++,同时使用的标准库更改为emscripten提供的标准库
  5. emcmake cmake ..
  6. # 再执行make进行编译,编译后可以发现build目录中生成了hello.wasm文件
  7. make
复制代码
使用wasmtime-cli运行hello.wasm文件
  1. > wasmtime hello.wasm
  2. hello, world!
复制代码
4 小试牛刀

实验场景

需要测试Rust代码被编译成wasm,C++代码被编译成wasm,在wasmtime中正确运行。其中C++代码可以调用Rust代码中的函数,然后外部可以调用C++代码中的函数。
Rust项目编译成wasm

创建一个项目叫做demo-rust-wasmtime
  1. cargo new demo-rust-wasmtime --lib
复制代码
创建好的项目结构如下
  1. demo-rust-wasmtime
  2. ├── Cargo.lock
  3. ├── Cargo.toml
  4. └── src
  5.    └── lib.rs
复制代码
首先需要在Cargo.toml中配置生成的库为cdylib,这表示按照C语言的FFI来生成动态库,要想不同语言之间能够互相调用对方的函数,通常需要将不同的语言按照相同的FFI来进行编译,确保函数调用的方式是相同的。这里同时我们将Rust项目的名称修改为calc。
  1. [package]
  2. name = "calc"
  3. version = "0.1.0"
  4. edition = "2021"
  5. # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  6. [lib]
  7. crate-type = ["cdylib"]
  8. [dependencies]
复制代码
在lib.rs中实现我们需要的add函数
  1. #[no_mangle]
  2. pub extern "C" fn add(left: i32, right: i32) -> i32 {
  3.     left + right
  4. }
复制代码
这里有两个地方需要注意:
C语言的调用约定规定了函数参数的传递方式、返回值的处理方式以及堆栈的清理方式。
这样就定义好了Rust项目中可以让外部使用的add方法。
我们使用如下命令对项目进行编译
  1. cargo build --target wasm32-unknown-unknown
  2. # 或
  3. cargo build --target wasm32-wasi
复制代码
这里两种target都可以使用,因为我们的项目中并没有使用任何系统的API,所以通常使用第一种target即可。
编译后可以在target/wasm-xxx/debug/目录下看到生成的calc.wasm文件。
可以使用wasmtime-cli实验一下是否能够调用add方法:
  1. > wasmtime calc.wasm --invoke add 101 202
  2. warning: using `--invoke` with a function that takes arguments is experimental and may break in the future
  3. warning: using `--invoke` with a function that returns values is experimental and may break in the future
  4. 303
复制代码
可以看到已经正确输出了结果,说明这个Rust项目已经被正确编译成了wasm。
C++项目编译成wasm

创建一个项目叫做demo-cpp-wasmtime,使用cmake作为构建工具,其目录结构如下
  1. demo-cpp-wasmtime
  2. ├── CMakeLists.txt
  3. ├── toolbox.cpp
  4. └── toolbox.h
复制代码
正如第3节讲到的,我们需要使用emscripten工具链代替gcc工具链来将这个C++项目编译成wasm。
cmake配置

因此我们需要按照如下方式配置CMakeLists.txt文件
  1. cmake_minimum_required(VERSION 3.26)
  2. project(demo_cpp_wasmtime)
  3. add_definitions(-std=c++17)
  4. set(CMAKE_CXX_STANDARD 17)
  5. if (DEFINED EMSCRIPTEN)
  6.     add_executable(toolbox toolbox.cpp toolbox.h)
  7.     set(CMAKE_EXECUTABLE_SUFFIX  ".wasm")
  8.     set_target_properties(toolbox PROPERTIES COMPILE_FLAGS "-Os -s SIDE_MODULE=1")
  9.     set_target_properties(toolbox PROPERTIES LINK_FLAGS "-Os -s WASM=1 -s SIDE_MODULE=1 -s STANDALONE_WASM --no-entry")
  10. else()
  11.     add_library(toolbox toolbox.cpp)
  12. endif ()
复制代码
这里有几点需要注意的
代码实现

toolbox.h头文件如下
  1. #pragma once
  2. extern "C" {
  3. int foo(int right);
  4. }
复制代码
类似Rust,这里我们声明了一个函数foo,并使用extern "C"表示这个foo函数需要按照C语言ABI进行编译。
接下来是toolbox.cpp的实现
  1. #ifdef __EMSCRIPTEN__#include <wasmtime.hh>#else#define EMSCRIPTEN_KEEPALIVE#define EM_IMPORT(NAME)#endifextern "C" {EM_IMPORT(add) int add(int a, int b);}extern "C" {EMSCRIPTEN_KEEPALIVE int foo(int right) {    return add(1, right);}}
复制代码
下面解释一下代码中的几个宏的作用:
使用EM_IMPORT(add)宏告诉编译器,这里声明的add方法其具体实现来自于其他模块,具体就是来自于env模块中的add函数。因此这里声明的add方法其实可以起任意的名字,只要签名与env模块中的add方法相同即可。
编译

使用如下命令进行编译
  1. # 在项目根目录下
  2. mkdir build
  3. cd build
  4. emcmake cmake ..
  5. make
复制代码
编译后在build目录下会生成toolbox.wasm二进制文件。
我们可以使用wasm2wat命令将编译好的wasm二进制文件转换成可读的wat文件来看一下生成的代码的结构
如果没有安装wasm2wat命令可以使用一下命令来安装
  1. sudo apt install wabt
复制代码
执行wasm2wat toolbox.wasm -o toolbox.wat命令后,可以打开toolbox.wat文件查看其结构如下
  1. (module
  2.   (type (;0;) (func (param i32 i32) (result i32)))
  3.   (type (;1;) (func))
  4.   (type (;2;) (func (param i32) (result i32)))
  5.   (import "env" "add" (func (;0;) (type 0)))
  6.   (func (;1;) (type 1))
  7.   (func (;2;) (type 2) (param i32) (result i32)
  8.     i32.const 1
  9.     local.get 0
  10.     call 0)
  11.   (export "__wasm_call_ctors" (func 1))
  12.   (export "__wasm_apply_data_relocs" (func 1))
  13.   (export "foo" (func 2)))
复制代码
可以看出,代码中import "env" "add"表示add函数来自env module的add函数。同时export "foo"表示toolbox.wasm对外暴露了foo函数。
wasmtime项目

wasmtime项目可以使用wasmtime支持的各种语言实现,这里我们以C++为例,看看如何将前面两个项目生成的.wasm文件调用起来。
创建一个项目叫做demo-run,使用cmake进行项目构建,其目录结构如下
  1. demo-run
  2. ├── CMakeLists.txt
  3. └── main.cpp
复制代码
cmake配置

wasmtime项目可以使用gcc工具链进行编译,因此它的CMakeLists.txt可以正常进行配置
  1. cmake_minimum_required(VERSION 3.26)
  2. project(demo_run)
  3. set(CMAKE_CXX_STANDARD 17)
  4. add_executable(demo_run main.cpp)
  5. target_link_libraries(demo_run PUBLIC wasmtime)
复制代码
因为我们需要在代码中使用wasmtime的库,因此这里需要使用target_link_libraries(demo_run PUBLIC wasmtime)将wasmtime链接进来。这也就要求必须先按照第2节中的安装方式配置好wasmtime的环境变量。
代码实现

具体wasmtime提供的每个API的用法在这里不多做赘述,具体可以参考wasmtime官方文档和官方提供的examples
[code]#include <wasmtime.hh>#include <wasmtime.hh>#include <wasmtime.hh>using namespace wasmtime;std::vector readFile(const char *name) {    std::ifstream watFile(name, std::ios::binary);    std::vector arr;    char byte;    while (watFile.get(byte)) {        arr.push_back(byte);    }    return arr;}int main() {    std::cout




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4