WebAssembly 与emscripten 入门指南

什么是WebAssembly?

  1. 是一个可移植、体积小、加载快并且兼容 Web 的全新格式
  2. wasm是体积小且加载快的二进制格式, 其目标就是充分发挥硬件能力以达到原生执行效率
  3. 运行在一个沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在web环境中,WebAssembly将会严格遵守同源策略以及浏览器安全策略。
  4. 中被设计成无版本、特性可测试、向后兼容的。WebAssembly 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。当然,WebAssembly 不仅可以运行在浏览器上,也可以运行在非web环境下。
  5. 支持语言: c/c++ 、rust、原始的webassembly S表达式文本、AssemblyScript(TypeScript-like)、Go 。 其他语言(python,java,scala,kotlin等诸多语言也有工具实验性支持)。

webassembly官网
mdn相关文档

浏览器支持情况

image.png

webassembly 特性

  1. 计算速度快,性能高,编译成wasm后的代码性能接近原生
  2. 可以使用c++/c/go 众多的三方库来前端处理复杂任务与计算 (opencv、FFmpeg等)
  3. 不需要垃圾回收机制,手动管理内存
  4. 通过wasm的内存与JavaScript 通信

应用场景

  • 将 C、C++、Rust 等语言编写的程序移植到浏览器
  • 图形图像处理领域(如OCR识别),如页游、数据可视化等
  • 音视频编解码识别、AI等等
  • 解压、压缩 等对性能要求高的需求

webassembly 基本概念及使用

几个概念

  • Module 一个“代码单元”。包含编译好的二进制代码。可以高效的缓存、共享。未来可以像一个ES2015模块一样导入/导出
  • Memory 内存,连续的,可变大小的字节数组缓冲区。可以理解为一个“堆”
  • Table 连续的,可变大小的类型数组缓冲区 现在table只支持函数引用类型,可以类比为一个“栈”
  • Instance 在Module基础上,包含所有运行时所需状态的实例,如果把Module类比为一个cpp文件,那么Instance就是链接了dll的exe文件

c代码-->借助工具编译为wasm

#include<stdio.h>
void fibonacci(int n)
{
 int first = 0, second = 1, next;
 for (int i = 0; i < n; i++)
 {
  next = first + second;
  first = second;
  second = next;
 }
}

load wasm 文件,获取 instance 实例

function load(path) {
 return fetch(path)
  // 获取二进制buffer
  .then(res => res.arrayBuffer())
  // 编译&实例化,导入js对象
  .then(bytes => WebAssembly.instantiate(bytes, importObj))
  // 返回实例
  .then(res => res.instance)
}

从instance中获取导出的文件

const fibonacci_wasm = instance.exports._fibonacci

上述代码重复计算一百万次斐波那契数列46项(47项会溢出),结果如下:
- C:3ms
- JS: 70ms
- WebAssembly:11ms

** 引用自 - [1] https://blog.csdn.net/m549393829/article/details/81839822

emscripten 封装好了上述获取实例的方法使用更简单,如 emcc 编译后的js文件 abc.js

import myModule from "../asm/abc.js";
myModule().then(zModule => {
      this.zModule = zModule;
  });
// 此时 zModule 就包含你导出的c方法及,emscripten 导出的常用方法

使用Emscripten编译并使用流程

  1. 安装Emscripten 环境 (略) 详见 emscripten官网
  2. 阅读C/C++ 三方库文档
  3. 编写C函数,用于调用库中的方法
  4. emcc命令编译c语言为wasm 及 封装的js胶水代码
  5. 编写胶水代码,用于C语言与js通信,js中调用c函数 (直接调用只能传递int值,传递其他类型值需要借助内存处理)

Vue中使用

方式一: 将wasm 与 js文件放到如cdn或服务器
方式二: 将wasm 与 js封装 为库,发布到 npm使用
方式三: 本地使用,

  • wasm文件并不会被webpack打包进dist,使用 url-loader 将 wasm 只作文静态文件路径 注意:import 下wasm文件确保被打包进dist
  • 可以放在vue的public文件下

Emscripten 编译命令

emcc命令指引

emcc -
优化flag,它们-O0,-O1,-O2,-Os,-Oz,-O3。 对应不同优化级别

-s OPTION=VALU 传给编译器的所有涉及到JavaScript代码生成的选项

emcc simple/helloword.c -o output/hellow.js \                                             
-s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall','abc,'_malloc','_free']" \   #导出的函数,abc为自己写的c语中的函数,其他为emscriten自带的                        
-s MODULARIZE=1   # 模块化,生成闭包函数
-s ENVIRONMENT="web" \
-s ALLOW_MEMORY_GROWTH=1 \  #开启可变内存
-s FORCE_FILESYSTEM=1   # 强制启用em的虚拟文件系统
-s RESERVED_FUNCTION_POINTERS #保留函数表指针

JS 类型化数组 与 buffer,与Blob

ArrayBuffer是一个构造函数,可以分配一段可以存放数据的连续内存区域

var buffer = new ArrayBuffer(16);  //创建一个连续16字节的内存缓冲
视图类型 说明 字节大小
Uint8Array 8位无符号整数 1字节
Int8Array 8位有符号整数 1字节
Uint8ClampedArray 8位无符号整数(溢出处理不同) 1字节
Uint16Array 16位无符号整数 2字节
Int16Array 16位有符号整数 2字节
Uint32Array 32位无符号整数 4字节
Int32Array 32位有符号整数 4字节
Float32Array 32位IEEE浮点数 4字节
Float64Array 64位IEEE浮点数 8字节
// 创建一个视图,此视图把缓冲内的数据格式化为一个32位(4字节)有符号整数数组
var int32View = new Int32Array(buffer);
// 我们可以像普通数组一样访问该数组中的元素
for (var i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}
  • Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。

    Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。

var aBlob = new Blob( array, options )
/** 例如 */
const blob = new Blob([int32View], {
 type: "application/zip"
});
  • array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings会被编码为UTF-8。

  • options

    是一个可选的

    BlobPropertyBag
    

    字典,它可能会指定如下两个属性:

    • type,默认值为 "",它代表了将会被放入到blob中的数组内容的MIME类型。
    • endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个: "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持blob中保存的结束符不变

1. 在C中调用JS函数之addFunction

Emscripten提供了多种在C环境调用JavaScript的方法,包括:

  1. EM_JS/EM_ASM宏内联JavaScript代码

  2. emscripten_run_script函数

  3. JavaScript函数注入(更准确的描述为:“Implement C API in JavaScript”,既在JavaScript中实现C函数API)

  4. 使用addFunction将函数指针传到C代码中调用

    第一个字符表示函数的返回类型,其余字符表示参数类型

    • 'v': void type
    • 'i': 32-bit integer type
    • 'j': 64-bit integer type (currently does not exist in JavaScript)
    • 'f': 32-bit float type
    • 'd': 64-bit float type

☆☆☆ webassembly 与 c 的通信

js 与 c的通信主要借助 webassembly 中的内存完成,基本思想是将一段数据的内存地址与长度传递到C中,c根据地址和长度取出内容。

ccall 与 ccwrap

如果直接使用C导出的函数,只能传递 number 的数据,如果使用了其他类型的需要借助ccall/cwrap

以下摘自https://emscripten.org/docs/api_reference/preamble.js.html

ccall(ident,returnType,argTypes,args,opts )

从JavaScript调用已编译的C函数。

该函数从JavaScript执行已编译的C函数,并返回结果。C ++名称处理意味着无法调用“正常”的C ++函数。该函数必须在.c文件中定义,或者是使用定义的C ++函数。extern "C"

returnType并argTypes让您指定参数的类型和返回值。可能的类型是"number","string","array",或"boolean",其对应于相应的JavaScript类型。使用"number"任何数值类型或C指针,string对于Cchar*表示字符串,"boolean"对于一个布尔类型,"array"为JavaScript阵列和类型数组,含有8位整数数据-即,数据被写入的8位整数的C数组; 特别是如果您在此处提供类型化数组,则它必须是Uint8Array或Int8Array。如果要接收其他类型的数据数组,则可以手动分配内存并对其进行写入,然后在此处提供一个指针(作为"number",因为指针只是数字)。

// Call C from JavaScript
var result = Module.ccall('c_add', // name of C function
  'number', // return type
  ['number', 'number'], // argument types
  [10, 20]); // arguments

总结: 传递的参数 只能为 字符串,数字,及Uint8Array或Int8Array

cwrap(ident,returnType,argTypes )

返回C函数的本机JavaScript包装器。

这类似于,但是返回一个JavaScript函数,该函数可以根据需要多次重复使用。C函数可以在C文件中定义,也可以是使用(防止名称修改)定义的C兼容C ++函数。ccall()extern "C"

// Call C from JavaScript
var c_javascript_add = Module.cwrap('c_add', // name of C function
  'number', // return type
  ['number', 'number']); // argument types

// Call c_javascript_add normally
console.log(c_javascript_add(10, 20)); // 30
console.log(c_javascript_add(20, 30)); // 50

emscripten cwrap 的胶水文件源码如下

function cwrap(ident, returnType, argTypes, opts) {
  return function() {
    return ccall(ident, returnType, argTypes, arguments, opts);
  }
}

可以看出,其本质 还是ccall,只是返回了函数方便调用

ccall源码如下

function ccall(ident, returnType, argTypes, args, opts) {
  // For fast lookup of conversion functions
  var toC = {
    'string': function(str) {
      var ret = 0;
      if (str !== null && str !== undefined && str !== 0) { // null string
        // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0'
        var len = (str.length << 2) + 1;
        ret = stackAlloc(len);
        stringToUTF8(str, ret, len);
      }
      return ret;
    },
    'array': function(arr) {
      var ret = stackAlloc(arr.length);
      writeArrayToMemory(arr, ret);
      return ret;
    }
  };

  function convertReturnValue(ret) {
    if (returnType === 'string') return UTF8ToString(ret);
    if (returnType === 'boolean') return Boolean(ret);
    return ret;
  }

  var func = getCFunc(ident);
  var cArgs = [];
  var stack = 0;
  assert(returnType !== 'array', 'Return type should not be "array".');
  if (args) {
    for (var i = 0; i < args.length; i++) {
      var converter = toC[argTypes[i]];
      if (converter) {
        if (stack === 0) stack = stackSave();
        cArgs[i] = converter(args[i]);
      } else {
        cArgs[i] = args[i];
      }
    }
  }
  var ret = func.apply(null, cArgs);

  ret = convertReturnValue(ret);
  if (stack !== 0) stackRestore(stack);
  return ret;
}

可以看出,传递字符串及数组的本质是 1.申请一定长度的空间(单位字节),得到空间的初始地址 2.将数据写入内存
接收数据 借助c中的指针(地址),从内存取出,

emscripten 封装了一堆根据指针(地址) 从内存中 写入、取出 字符串、文件 数据的放法,需要时自行文档及源码查阅。

例子:传递复杂的数据,如字符串数组到 c函数

循环申请空间,得到每个字符串的指针,并写入内存

  const nameList = ['ssdf','dsfsd','sdfs']; // 字符串数组
  const namePtrList = []; // 用于存放name指针
  nameList.forEach(v=>{
      const maxLen = nameList[i].length * 4 + 1; //c中字符串有 \0 为标志的结束符所以+1
      const namePtr = this.zModule._malloc(maxLen);
      namePtrList.push(namePtr);
      this.zModule.stringToUTF8(nameList[i], namePtr, maxLen); //emscripten 封装好的写入字符串到内存的方法
  })

借助指针把namePtrList当做普通数组传递到c

/**
 * 传递数据的时候要借助上文提到的类型化数组,对应大小的,转化为对应的类型化数组
 * 这里指针(地址)是32位,且不需要符号,所以用 32位无符号的 Uint32Array
 */
const namePtrListArr = new Uint32Array(namePtrList); 
const namePtrListPtr = this.zModule._malloc(namePtrListArr.length * 4); // ...
/**
 * @type {Int8Array} - HEAP8
 * @type {Uint8Array} -HEAPU8
 * ... 同理
 */
this.zModule.HEAPU32.set(namePtrListArr, namePtrListPtr / 4); //写入内存,第二个参数32位/4 ,16位/2 同理
// xxFun为c导出的函数
xxxFun(namePtrListPtr);

传递文件可以借助 emscripten的writefile 方法写入 虚拟文件系统,也可以将文件转化为类型化数组借助指针写入内存,方法同上

释放空间

emscripten 导出的 _malloc,_free 用于申请及释放空间,
在一段写入内存的数据不再使用后释放空间,相当于垃圾回收

this.zModule._free(namePtrListPtr); // 释放指针内存

问题与优化

内存是非常珍贵的硬件资源,用内存模拟文件系统是非常奢侈的行为。应考虑减少内存使用

2. [在web worker中使用webassembly](https://www.cntofu.com/book/150/zh/ch6-threads/ch6-02-sample.md

由于加载wasm的过程是同步耗时的,因此大的wasm文件可以借助web worker开启多线程使用
Worker 接口是 Web Workers API 的一部分,指的是一种可由脚本创建的后台任务,任务执行中可以向其创建者收发信息

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • WebAssembly 是一种可以使用非 JavaScript 编程语言编写代码并且能在浏览器上运行的技术方案。 ...
    hellomyshadow阅读 352评论 0 0
  • webAssembly设计的目的不是为了手写代码而是为诸如C、C++和Rust等低级源语言提供一个高效的编译目标。...
    flyrain阅读 2,225评论 0 0
  • 本次分享的文章是基于WebAssembly的探索与研究。最近需要做一个与加密相关的项目,想将后端的加密方案直接放到...
    Netwarps阅读 1,558评论 1 1
  • 上一篇文章分享了WebAssembly概念和基本使用,通过两个代码示例的分析对WebAssembly有了大致的了解...
    Netwarps阅读 1,752评论 0 0
  • 推荐指数: 6.0 书籍主旨关键词:特权、焦点、注意力、语言联想、情景联想 观点: 1.统计学现在叫数据分析,社会...
    Jenaral阅读 5,685评论 0 5