Rust开发WebAssembly在Html和Vue中的应用后篇

Posted 一码超人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Rust开发WebAssembly在Html和Vue中的应用后篇相关的知识,希望对你有一定的参考价值。

【建议先看】继上一篇【Rust开发WebAssembly在Html和Vue中的应用】遗留下来的问题

Rust开发WebAssembly在Html和Vue中的应用_一码超人的博客-CSDN博客

本文讲述Vue2与H5版uniapp如何引入rust webassembly的应用流程 

在上一文中末尾,我说过vue2在引入胶水js后执行会报错,如下:

 

         说真的,我前端并不是很好,尤其对手脚架的相关操作,在查询import.meta了解到,是一个给javascript模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL。但是为什么会报“当前未启用对实验语法“importMeta”的支持”?说真的,vue生态这么多年了,我想着百度一下吧。居然没有人问这个相关的问题???当然在座的各位如果有这个问题的解决方案可以留言或者加我好友私聊探讨一下,我是那种有问题无法正规处理就很难受那种~ 也不知道是不是和vite有关。

废话不多说了,进入本文主题。

Vue2应用Rust wasm

创建vue2项目

 在根目录与public下都放入pkg生产文件

 进入代码执行wasm函数

<script>
import HelloWorld from './components/HelloWorld.vue'
import init, add, my_str from "../pkg/mylib.js";
export default 
  name: 'app',
  components: 
    HelloWorld
  ,
  async created()
	  console.log(111)
	  this.in11()
  ,
  methods:
	  async in11()
		  await init()
		  console.log(add(1,2))
		  console.log(my_str("5LiA56CB6LaF5Lq6"))
	  
  

</script>

直接npm运行,会报错哦!

 如上述,提示不支持importMeta,下方也给了建议,安装“@babel/plugin-syntax-import-meta”插件,我尝试做了下,最后会提示加载wasm文件问题,再无下文了。

那有没有更暴力简单一点的方法呢?肯定是有的!

首先先了解import.meta.url返回的是什么?如下:

 改写js胶水文件,大不了不用它了好不好?

 注意根目录下的pkg/mylib.js与public下的pkg/mylib.js都要做如此修改~~~~

		//import.meta.url返回的数据:http://127.0.0.1:8080/pkg/mylib.js
		var url = location.origin + '/pkg/mylib.js'; //http://127.0.0.1:8080/pkg/mylib.js
        input = new URL('mylib_bg.wasm', url);

然后npm运行,就不会报错了,这个在手脚架非常通用!不过在与后端整合时需要注意路由问题!!!!!!!注意路由问题!!!!!!!!!注意路由问题!!!!!!!

也就是说线上部署后 https://域名/pkg/mylib.js一定是要能访问到的!

不行就在站点运行跟目录扔个pkg!!!!

运行后结果

 虽然很奇葩,但是怪我前端能力也就是这样了。不过通用性很好,甚至支持uniapp H5!

UNIAPP应用Rust wasm

 创建一个uniapp

 

 注意这里的胶水js一样需要如上修改!

		//import.meta.url返回的数据:http://127.0.0.1:8080/static/pkg/mylib.js
		var url = location.origin + '/static/pkg/mylib.js'; //http://127.0.0.1:8080/static/pkg/mylib.js
        input = new URL('mylib_bg.wasm', url);

 当然情况也是一样,部署站点后,一定是根据这个地址能访问到这个js!不行就在站点根目录扔个/static/pkg!!!

运行wasm函数

<script>
	import init, add, my_str from "@/static/pkg/mylib.js";
	export default 
		data() 
			return 
				title: 'Hello'
			
		,
		onLoad() 
			this.inii()
		,
		methods: 
			async inii()
				await init()
				console.log(add(1,2))
				console.log(my_str("5LiA56CB6LaF5Lq6"))
			
		
	
</script>

 运行结果

 总结

总算是心里好受一些,不过很丑陋,对于部署线上非分离式的就很难受了【路由与静态资源访问问题】。所以如果有前端大神可以提出一下建议,能否有一个好的方案解决?比如支持importMeta、或者importMeta的替代方案?

希望能有一位关注的大神,来优化该问题。

更新日期:2022/8/8

解决vue2不支持importMeta语法问题

vue手脚架安装 @open-wc/webpack-import-meta-loader 基座

npm i @open-wc/webpack-import-meta-loader

安装以后配置webpack

创建vue.config.js

 

 

const webpack = require('webpack');
module.exports = 
  configureWebpack: 
    module: 
      rules: [
        
          test: /\\.js$/,
          use: 
            loader: '@open-wc/webpack-import-meta-loader',
          ,
          // include: path.resolve(__dirname, 'node_modules/cesium/Source')
        ,
      ]
    ,
    plugins: [

    ],
  

之后pkg/mylib.js胶水文件中的import.meta.url就不会报不支持的错误了!

 

input = new URL('mylib_bg.wasm', import.meta.url);

这个是正规的解决方案,解决临时方案的部署缺陷。

整体流程:

1、安装支持import.meta基座插件。

2、配置webpack引入插件。

3、rust生成的pkg无需任何改动直接拖进项目。

4、完美启动无报错执行wasm函数。

入门 Rust 开发 WebAssembly

本文来自 AirCloud 的知乎投稿:https://zhuanlan.zhihu.com/p/104299612


写在前面

可以用于开发 WebAssembly 的语言比较多,笔者之前也尝试过 AssemblyScript、C++、Rust,相对来说,使用 Rust 开发在开发效率和便捷性、包体积大小等方面还是有很大优势的,因此,笔者也建议使用 Rust 来作为 WebAssembly 的开发语言。

Rust 开发 WebAssembly 非常方便,实际上官方周边文档已经比较全面和友好了,而这篇文章主要有两个目的:

  • 帮助大家快速上手:目前有些资料还是比较零散的,这里希望将各个资料中的一些东西串联起来,特别是开发者比较关心的入门开发、调试等各个过程。

  • 帮助大家建立 Rust 开发 WebAssembly 的心智模型:由于使用 Rust 入门开发 WebAssembly 已经足够简单,官方实际上把很多内容进行了封装,比如 Rust 和 JS 交互的部分等,而本文对比较关键的各个部分原理也进行讲解,而不仅仅是如何开发,从而让大家对原理也有一个了解。

本文的目标读者:

  • 对前端有一定经验,并且对 WebAssembly 感兴趣的同学

  • 有 Rust 的开发经验,或对使用 Rust 开发 WebAssembly 感兴趣的同学

  • 已经使用了 Rust 开发 WebAssembly,尚未深入,但是对整体原理感兴趣的同学

本文看后,读者可以基本掌握:

  • 搭建一个简单的 Rust webassembly 开发环境

  • 编写代码完成 Rust 和 js 的交互需求,并了解原理

  • 调试和错误处理

Rust+WebAssembly 的能力

在开始开发之前,我们可以先大致了解下 Rust+webassembly 能干些什么:

  • 可以使用 Rust std,可以使用 Rust 的大多数第三方库(部分涉及多线程的,可能会有一些问题,关于 webassembly 多线程后续会写文章单独进行讲解)。

  • 可以调用几乎任何 JS 侧声明的方法,也可以暴露方法给 JS 调用。

  • 可以和 JS 侧互相”传递“几乎任何的数据类型,包括但不限于基本的数字、字符串、对象、Dom对象等。

  • 可以直接在 Rust 侧“操作”Dom,甚至已经出现了 Rust 版本的 react

起步开发

我们的第一个目标,肯定是希望能最快看到 hello-world,接下来我们需要一步步操作:
安装 wasm-pack,wasm-pack 是将 Rust 打包成 wasm 的命令行工具:

curl https://Rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

然后我们需要安装 cargo-generate,后续在样板代码生成的时候需要:

cargo install cargo-generate

接下来,我们需要建立一个项目,这里我们可以使用 create-wasm-app 这个工具,做前端开发的同学想必对这类 create-xx 应该比较熟悉了。

我们可以直接使用 npm init 目录来生成一个样板库,并安装依赖:

npm init wasm-app ./myRust && cd myRust && npm i

这个时候我们可以使用 npm start 来运行代码并且在浏览器访问了,这个时候,我们应该可以看到一个 alert 弹框。

不过当我们看入口代码发现,这个样板库中的 wasm 部分,是直接引入的一个 npm 包:

import * as wasm from "hello-wasm-pack";

这显然是不能符合我们的需求的,因为我们是需要开发 Rust 部分代码的,而不仅仅是前端引入。

这个时候,我们可以借助 wasm-pack-template,在项目目录下建立一个 Rust wasm 项目文件:

cargo generate --git https://github.com/Rustwasm/wasm-pack-template.git --name hello

这样我们所有跟 Rust 有关的 wasm 文件都放在 hello 下面。

我们可以在 hello/src/lib.rs 下面随便修改一点 greet 函数的内容(应该只有一行,随便改),然后运行 wasm-pack build

接下来我们修改我们 js 代码的引入:

import * as wasm from "./hello/pkg/hello";

接下来我们再运行 npm start,应该能看到预期的内容了。

于是,我们已经搭建好了一个方便的 Rust -> wasm 的运行环境,这个环境虽然相对简陋无法直接在实际项目中使用,但对于我们调试和建立起对代码开发的认知,已经足够。

Rust 和 JS 代码交互

这里的内容是我们在日常开发中使用比较多的,我们的 wasm 模块大多作为 JS 的 enhancment,自然少不了与 JS 的代码交互,这里我们对此进行分析。

函数调用

暴露函数给 JS 调用

如果是需要暴露在 JS 中调用的函数,我们只需要使用 wasm_bindgen 过程宏即可,一个最简单的例子:

#[wasm_bindgen]
pub fn get_version() -> i32 {
1
}

这个函数经过 wasm-pack 打包之后,可以直接挂到 wasm 模块实例上,当然,我们打包后的代码还会生成一个 js wrapper(所有的 wasm 函数,都会有对应的 js wrapper 函数供调用方使用),最后的返回结果类似如下:

/**
* @returns {number}
*/
export function get_version() {
var ret = wasm.get_version();
return ret;
}

wasm_bindgen 可以通过传递参数来实现更加复杂的功能,本文章暂不展开,具体可以参考这里。

调用 JS 的函数

我们可以在 Rust 层调用 js 几乎任意的函数,只需声明即可,例如调用 js 中的 console.log:

#[wasm_bindgen]
extern {
#[wasm_bindgen(js_namespace = console)]
pub fn log(s: &str);
}

其原理是,在工具链解析的时候会在 js wrapper 层生成一个对应的函数,然后这个对应的函数会在 wasm 实例化的时候通过 importObject 传递进去(参考这里的参数传递)。

export const __wbg_log_20c778ed882114c1 = function(arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
};

Rust 传值给 JS

在了解值传递的过程前,我们需要知道:

  • wasm 一个模块只有一个线性内存,这个线性内存是一个在 JS 中分配的 TypedArray,所有的这个模块的相关内容都是存储在这里(原话:(the stack, the heap, everything))。

  • 对于 Rust - wasm 来说,虽然 JS 可以管理这段线性内存,但是为了保证内部的一致性,所有内存具体分配的操作都是在 Rust 侧完成,即使 JS 需要写内存,也是调用 Rust 的内存分配函数并传递长度,从 Rust 这里拿到一个偏移量,从而写入。

因此,如果 wasm 需要传递值给 js,也是写入到线性内存的某处,给 JS 读取:

  • 如果是简单的数字、字符串,可以直接返回或转成 buffer 后给 JS 读取,一般官方实现了相关 trait,我们直接使用即可。

  • 如果是比较复杂的类,需要先序列化成字符串或数组等可序列化的内容(JSON、protobuf等),然后给 JS 调用,具体可以参考下面的使用说明。

如果需要使用 JSON 序列化来返回对象给 JS,我们需要修改我们的 cargo.toml 的相关依赖和 features:

wasm-bindgen = { version = "0.2.58", features = ["serde-serialize"]  }
serde = { version = "1.0.80", features = ["derive"] }
serde_derive = "^1.0.59"

然后在代码中调用:

#[derive(Serialize, Deserialize)]
pub struct Dog {
index: i32
}

#[wasm_bindgen]
pub fn get_dog() -> JsValue {
let dog = Dog {
index: 10
};
JsValue::from_serde(&dog).unwrap()
}

一般情况而言,我们在 Rust 中是没有办法返回 struct 等一些复杂的数据结构给 js 的,不过,我们也可以通过实现相关 trait 来完成返回一个 struct:

pub struct Duck {
index: i32
}
impl wasm_bindgen::describe::WasmDescribe for Duck {
fn describe() {
u32::describe()
}
}
impl wasm_bindgen::convert::IntoWasmAbi for Duck {
type Abi = u32;
fn into_abi(self) -> u32 {
self.index as u32
}
}
#[wasm_bindgen]
pub fn get_version() -> Duck {
Duck {
index: 4
}
}

我们可以看出,这样其实还是比较麻烦的,而且效率也不高,所以我们应该尽量减少复杂数据结构的传递。

Rust 使用 JS 传递的值 && 调用 web 浏览器接口

Rust 使用 JS 传递的值,对于简单类型(数字、字符串)来说,其流程一般是:

  • 大部分数字类型,都可以直接传递,也不需要写入线性内存

  • 少量的数字类型(比如 i64,实际上对应到 js 是 BigInt),会根据高低位转化成两个数字,也可以认为是直接传递的

  • 字符串类型,一般比较复杂,流程分下面几个步骤:

    • 通过 TextEncoder 以 utf-8 的形式编码成 buffer

    • 调用 Rust wasm 提供的的 malloc 函数,拿到一个指针

    • 把之前的 buffer 拷贝到对应的位置

我们可以看到,这种转化特别是字符串的转化,还是比较麻烦的,而实际上我们在一个 wasm 模块中,有的时候并不需要把 js 侧的内容完全拷贝过去,也不会直接使用到 js 的变量,而只是暂时存起来供后面调用,实际上后面也是调用 js 的函数调用,这里流程大概是:

  • Rust层先存起来一个JS 对象

  • 后面调用 JS 侧函数,仍然JS层调用这个 JS 对象

实际上根本不需要把整个对象放到 Rust 中。

另外有的时候,我们没有办法也不能把一个 js 对象完全传递给 Rust wasm模块中(例如一个 dom 对象),所以,在 Rust wasm 中实际上还有一种 js 变量的“借用”机制, 下面我们来对此进行分析。

我们的 demo 场景是在 Rust 中操作一个 dom 并写入 innerHTML,代码如下:

实际上,getElementById 这些过程在 Rust 侧做也都是可以的,但是这里我们为了突出重点,进行了简化。

// Rust:
#[wasm_bindgen]
pub fn set_dom_inner(dom: HtmlElement) {
dom.set_inner_html("This is from Rust");
}


// js:
wasm.set_dom_inner(document.getElementById('wasm'));


// html:
<p id="wasm"></div>

这个代码中的 Rust 部分,编译出来的 js-wrapper 代码如下:

/**
* @param {any} dom
*/
export function set_dom_inner(dom) {
wasm.set_dom_inner(addHeapObject(dom));
}

这里我们可以看到,其并没有通过一番转化直接把dom“传递进去”(实际上也没法这样做),而是调用了 addHeapObject :

function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
if (typeof(heap_next) !== 'number') throw new Error('corrupt heap');
heap[idx] = obj;
return idx;
}
const ret = getObject(arg0).createElement(getStringFromWasm(arg1, arg2));
let index = addHeapObject(ret);

这个函数,实际上就是保持住这个对象的引用,防止在 js 侧被垃圾清除,同时传递给 Rust 侧一个索引,在 Rust 层直接存储这个索引即可( Rust 会生成一个 JsValue 结构体,用来存储这个 u32 的索引)。

当然了,这个时候我们还是有一个问题,既然这个变量被挂到了 heap 上,那肯定也有一个清除机制,否则就是内存泄漏了。

清除机制当然是有的:

function dropObject(idx) {
if (idx < 36) return;
heap[idx] = heap_next;
heap_next = idx;
}
// heap 这里其实是一个链表,把所有为空的串起来了。这样被解除引用了的,会被垃圾回收


function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
export const __wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};

在 wasm 侧,通过调用 import 进来的 __wbindgen_object_drop_ref 最终调用 dropObject 进行清除,__wbindgen_object_drop_ref 的调用是在对应对象在 Rust 析构的时候进行(上面的 dom 对象传递到 rust 后,就是一个 JsValue struct):

pub struct JsValue {
idx: u32,
_marker: marker::PhantomData<*mut u8>, // not at all threadsafe
}
// many other things...
impl Drop for JsValue {
#[inline]
fn drop(&mut self) {
unsafe {
// We definitely should never drop anything in the stack area
debug_assert!(self.idx >= JSIDX_OFFSET, "free of stack slot {}", self.idx);
// Otherwise if we're not dropping one of our reserved values,
// actually call the intrinsic. See #1054 for eventually removing
// this branch.
if self.idx >= JSIDX_RESERVED {
__wbindgen_object_drop_ref(self.idx);
}
}
}
}
注:以上这些内容。wasm-pack 工具链都会帮助我们自动完成

代码调试与错误处理

比较遗憾的是,目前 WebAssembly 还没有办法直接进行断点调试,也没有办法从 panic! 中恢复(来自官方团队:in wasm panics always get translated into aborts, so you can't catch them)。

目前我们能做的事情有:

  • 调用 console 相关的方法打印内容到控制台

  • 补获 panic!,虽然此时不能恢复了,但是可以打印堆栈到控制台,也可以调用 JS 的函数,上报堆栈和日志等内容

  • 可以返回错误给 JS 用于 try catch(推荐做法)

借助以上功能,实际上我们已经可以编写出比较稳妥的 wasm 包了。

在 Rust 中使用 console

在 Rust 中使用 console 对象上的方法和使用任何 JS 对象的方法一样,实际上非常简单:

#[wasm_bindgen]
extern {
#[wasm_bindgen(js_namespace = console)]
pub fn log(s: &str);
#[wasm_bindgen(js_namespace = console)]
pub fn info(s: &str);
#[wasm_bindgen(js_namespace = console)]
pub fn warn(s: &str);
#[wasm_bindgen(js_namespace = console)]
pub fn error(s: &str);
}

直接使用上面的 log 需要传递字符串引用,比较繁琐,我们可以实现一个声明宏来完成这个事情:

macro_rules! log {
($($t:tt)*) => (log(&("[W]".to_string() + &format_args!($($t)*).to_string())))
}

捕获 panic!

为了在 Rust 中捕获 panic,我们需要用到 console_error_panic_hook 这个库,然后我们在某个提供给 JS 的初始化函数中调用 console_error_panic_hook::set_once(); 或者提供给一个单独的函数给 JS 调用。

实际上,console_error_panic_hook 这个函数的代码非常少,在实际项目中,也可以自行修改源码,将项目中需要的 panic 处理通用化。

返回错误给 JS 进行 try catch

上面提到我们在 Rust 中虽然能捕获到 panic!,但是此时也只能做“通知”而不能恢复了,而在实际的编码中我们使用的更多的应该是 Result,一个简单的例子如下:

// Rust:
#[wasm_bindgen]
pub fn return_error() -> Result<i32, JsValue>{
return Err("This is a Js Value".into());
}

// js:
try {
wasm.return_error();
} catch(e) {
console.log('catch error:', e);
}
// catch error: This is a Js Value

我们还可以借助一些 Rust 错误处理库比如 error_chain 等,来更完善地返回错误。

<完>


以上是关于Rust开发WebAssembly在Html和Vue中的应用后篇的主要内容,如果未能解决你的问题,请参考以下文章

入门 Rust 开发 WebAssembly

Rust在2018年将专注于开发效率WebAssembly嵌入式等方面

在 Rust 程序和嵌入式 WebAssembly 运行时之间进行通信的最佳实践是啥?

2019 年,Rust 与 WebAssembly 将让 Web 开发更美好

招聘远程 - Rust / WebAssembly 工程师/开发者布道师

使用 Rust + WebAssembly 编写 crc32