WebAssembly体验之编码base64(AssemblyScript使用教程)

前言

WebAssembly 不用多说懂的都懂,将运算函数通过 c++ 等编译为二进制的 .wasm 文件后,再通过 JavaScript 的 WebAssembly Api 调用即可进行“快速”计算。

下面快速上手体验一把,不使用 c++ 等编译 .wasm 文件。

WebAssembly 中文网:WebAssembly-CN

WebAssembly 英文官网:WebAssembly-EN

使用

AssemblyScript

AssemblyScript 是 WebAssembly 社区的一个 JavaScript 解决方案,通过编写 TypeScript 来达到近似 C 语言的类型限制,完成预编译,进而做到编译出二进制 .wasm 文件。

AssemblyScript 官方项目:AssemblyScript / assemblyscript

那他与使用 c++ 等进行编译有什么限制呢,主要是两点:

  1. 类型不全面。因为是通过 ts 给出近似实现,所以目前不可以使用复杂类型(比如 RegExpTextEncoder 等),且 h5 新 api 大部分也不能使用。

  2. 不可以使用第三方依赖。

对于第一点类型限制,由于是使用 ts 编写所以不支持的类型会报错还是很友好的,对于第二点,意味着所有功能都需要我们纯手撕,另外很多 h5 api 用不了的情况下,更增加了复杂性。

搭建环境

官方文档:AssemblyScript

先初始化项目:

	yarn init -y

安装两个基本依赖:

	yarn add @assemblyscript/loader
	yarn add -D assemblyscript

其中 @assemblyscript/loader 是微型加载器,可以帮我们省去很多配置,即开即用。assemblyscript 是开发核心,内置了所有可用的类型声明(在 node_modules/assemblyscript/std 下)。

初始化项目:

	yarn asinit .

此时会打印将要生成的文件结构,输入 Y 确认,将得到一个基础开发目录:

我们关注的只是在 assembly/index.ts 内函数编写逻辑。

base64 逻辑实现

虽然 base64 是个小功能,但是痛点啪的一下就凸显出来了,很快啊。

我们不能使用第三方库,于是乎 js-base64 是不能用的。去把源码搬进来行不行?是不行的,因为 js-base64 使用了很多 h5 的 api 与 RegExp 正则替换,所以我们只能实现一个 乞丐版 的:编码 base64:

// ./assembly/index.ts
export class Base64 {

    _keyStr: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

    encode(input: string): string {
        var output = "";
        var chr1: i32, chr2: i32, chr3: i32, enc1: i32, enc2: i32, enc3: i32, enc4: i32;
        var i = 0;

        input = this._utf8_encode(input);

        while (i < input.length) {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);

            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;

            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            }
            else if (isNaN(chr3)) {
                enc4 = 64;
            }

            output = output +
                this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
                this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
        } // Whend 

        return output;
    } // End Function encode 

    _utf8_encode(string: string): string {
        var utftext = "";

        for (var n = 0; n < string.length; n++) {
            var c = string.charCodeAt(n);

            if (c < 128) {
                utftext += String.fromCharCode(c);
            }
            else if ((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }

        } // Next n 

        return utftext;
    }

}

export function getBase64(): Base64 {
    return new Base64()
} 

语法规则

编写 AssemblyScript 就要遵守他的规则,我们着重需要确定的是三个部分:

  1. 回归原始。不使用 h5 新 api ( atobTextEncoder 等),不接触复杂类型( RegExp 等),如果编辑器红线报错就是找不到相对应的类型了,说明这个类型无法使用。

  2. 注意数字类型。对于 number 类型,AssemblyScript 含有更具体的 i32i64f32 等类型(见官方文档 Types ),在相应的变量初始化时对其指定合适的类型。

  3. 明确基本类型。变量初始化要指明其基础类型,以便 AssemblyScript 预编译。

到此为止,我们完成了最核心的 AssemblyScript 逻辑编写。

此处需要我们注意两个问题:

  1. 上面这个逻辑是简单版的,比如换行等边界情况是没有考虑的。

  2. 为什么要写成单例模式,这么写是官方推荐的导出类写法,采用相同的引用节省资源。

构建 wasm

运行打包:

	yarn asbuild

之后就会在 ./build 下得到优化后和优化前的 .wasm 二进制文件:

实际调用

./test/index.js 内编写:

const loader = require("@assemblyscript/loader"); 

const fs =require('fs')

const text = '年轻人不讲武德'

loader.instantiate(
    fs.readFileSync('../build/optimized.wasm'),
  {env: { memory: new WebAssembly.Memory({initial:10, maximum:100})} }
).then(({ exports }) => {

  console.time("测试 wasm 速度: ")

  const { __getString, __retain, __newString, __release } = exports
  const { getBase64, Base64 } = exports
  const Base64Ptr = getBase64()
  const base64 = Base64.wrap(Base64Ptr)

  for(let i = 0;i<10000;i++) {
    const textPtr = __retain(__newString(text))
    const outputPtr = base64.encode(textPtr)
    // console.log(__getString(outputPtr))
    __release(textPtr)
    __release(outputPtr)
  }

  __release(Base64Ptr)

  console.timeEnd("测试 wasm 速度: ")

})

下面我们分块讲解:

loader.instantiate(
    fs.readFileSync('../build/optimized.wasm'),
    {env: { memory: new WebAssembly.Memory({initial:10, maximum:100})} }
)

作用:初始化一个微型加载器 loader ,他内置了很多预设,可以帮我们省去很多配置的功夫,往往我们只需要指定 env.memory 参数定义初始内存值即可(在这里不指定也可),当你需要更大内存运算时,记得指定。

参数1:这里第一个参数是读入 .wasm 文件,可以是 fs 本地读取,也可以是 fetch 远程拉取(在浏览器的情况)。

参数2:loader 的配置,正常情况可以不传,采用默认配置,更多配置详见 node_modules/@assemblyscript/loader/index.d.ts 中的 Imports 与官网说明。

.then(({ exports }) => {

  console.time("测试 wasm 速度: ")
  // ...
  console.timeEnd("测试 wasm 速度: ")

})

拿到异步加载的结果并结构得到 exports ,即为我们在 ./assembly/index.ts 导出的函数。

注:实际上 exports 并不只有我们导出的函数,他还有一些“辅助”函数,帮助我们更友好的使用 WebAssembly 。

  // 导出辅助函数
  const { __getString, __retain, __newString, __release } = exports
  // 导出我们编写的函数
  const { getBase64, Base64 } = exports
  // 获取 class 单例的指针
  const Base64Ptr = getBase64()
  // 将指针转为实际的 class 类
  const base64 = Base64.wrap(Base64Ptr)

在这里,我们先将辅助函数导出,为什么需要辅助函数?因为在 WebAssembly 中,不存在字符串和 class 等基本类型的概念,一切均为 buffer 与 number ,所以辅助函数的作用就是帮我们做了 string 和 class 的中间转化处理,真是非常友好了!

namedescription
__getString从一个 string 的指针获取实际的字符串值
__retain定义一个引用 id ,以便后续回收,多次调用的 id 会进行累加(并不需要我们维护)
__newString将实际字符串转为 string 的指针,以便传入
__release根据 __retain 释放引用

更多辅助函数请看官网说明:loader usage

  // 执行 10000 次编码
  for(let i = 0;i<10000;i++) {
  	// 获取一个 string 的指针
    const textPtr = __retain(__newString(text))
    // 传入指针得到返回值 string 的指针
    const outputPtr = base64.encode(textPtr)
    
    // 可以通过 __getString 方法将 string 指针转为实际的字符串
    // console.log(__getString(outputPtr))
    
    // 清理引用
    __release(textPtr)
    __release(outputPtr)
  }
  
  __release(Base64Ptr)

我们对其做 10000 次编码,实际中被编码的 text 会发生变化,可能时间会更长:

校验

我们打印一次出来看一下结果:

校验:

成功!

对比

下面我们用相同的代码进行测试直接使用的速度:

const { getBase64 } = require('./encode')

console.time("测试直接使用速度")

const base = getBase64()

for(let i = 0;i<10000;i++) {
  base.encode(text)
  // console.log(base.encode(text))
}

console.timeEnd("测试直接使用速度")

结果:

总结

目前 WebAssembly 主要是应用在音视频处理和网页游戏上,可以借助相关库的便利性,比如 ffmpeg 实现转码,音视频压缩,B 站视频上传过程即可选择封面等。

加上规范的不成熟,旧版本浏览器兼容问题,以及如此缓慢的速度,虽然不能否定,但也请不要太吹嘘 WebAssembly 了。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值