• WebAssembly 与emscripten 入门指南


    什么是WebAssembly?

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

    webassembly官网
    mdn相关文档
    浏览器支持情况
    image.png
    webassembly 特性

    计算速度快,性能高,编译成wasm后的代码性能接近原生
    可以使用c++/c/go 众多的三方库来前端处理复杂任务与计算 (opencv、FFmpeg等)
    不需要垃圾回收机制,手动管理内存
    通过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编译并使用流程

    安装Emscripten 环境 (略) 详见 emscripten官网
    阅读C/C++ 三方库文档
    编写C函数,用于调用库中的方法
    emcc命令编译c语言为wasm 及 封装的js胶水代码
    编写胶水代码,用于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",用于指定包含行结束符
    的字符串如何被写入。 它是以下两个值中的一个: "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持blob中保存的结束符不变
    
    1. 在C中调用JS函数之addFunction

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

    EM_JS/EM_ASM宏内联JavaScript代码
    
    emscripten_run_script函数
    
    JavaScript函数注入(更准确的描述为:“Implement C API in JavaScript”,既在JavaScript中实现C函数API)
    
    使用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 ''
    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中字符串有 为标志的结束符所以+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 的一部分,指的是一种可由脚本创建的后台任务,任务执行中可以向其创建者收发信息

  • 相关阅读:
    jQuery的end() 方法
    jQuery.extend 函数使用详解
    AutoMapper完成Dto与Model的转换
    IoC实践--用Unity实现MVC5.0的IoC控制反转方法
    IoC实践--用Autofac实现MVC5.0的IoC控制反转方法
    Unity依赖注入使用详解
    React+BootStrap+ASP.NET MVC实现自适应和组件的复用
    oracle基础开发工具及常用命令
    Cisco配置发送日志到日志服务器
    Redis讲解
  • 原文地址:https://www.cnblogs.com/lidabo/p/14693594.html
Copyright © 2020-2023  润新知