使用WebAssembly在客户端处理数据





WebAssembly(缩写为WASM)是一种用于在客户端的浏览器中运行预编译的二进制代码的技术。它于2015年首次引入,目前大多数现代浏览器都支持它



一种常见的用例是客户端在将文件发送到服务器之前对数据进行预处理。在本文中,我们将了解如何完成此操作。



开始之前



WebAssembly体系结构和一般步骤在此处此处进行了详细描述我们将只讨论基本事实。



使用WebAssembly首先要预先组装在客户端上运行已编译代码所需的工件。其中有两个:实际的二进制WASM文件本身和一个JavaScript层,您可以通过它调用导出到该文件的方法。



最简单的C ++代码示例



#include <algorithm>

extern "C" {
int calculate_gcd(int a, int b) {
  while (a != 0 && b != 0) {
    a %= b;
    std::swap(a, b);
  }
  return a + b;
}
}


对于汇编,使用Emscripten,除了此类编译器的主界面外,还包含其他标志,通过这些标志可以设置虚拟机的配置和导出的方法。最简单的启动如下所示:



em++ main.cpp --std=c++17 -o gcd.html \
    -s EXPORTED_FUNCTIONS='["_calculate_gcd"]' \
    -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]'


通过指定一个* .html文件作为一个对象,它告诉编译器也使用js控制台创建一个简单的html标记。现在,如果我们在收到的文件上启动服务器,我们将看到此控制台能够启动_calculate_gcd:







数据处理



让我们使用一个用C ++编写的库进行lz4压缩的简单示例来分析它。请注意,支持的多种语言并不止于此。



尽管该示例非常简单且具有一些综合性质,但这还是一个非常有用的示例,说明了如何使用数据。同样,您可以对它们执行任何足以增强客户端功能的操作:发送到服务器之前进行图像预处理,音频压缩,统计各种统计信息等等。



完整的代码可以在这里找到



C ++部分



我们使用现成的lz4实现然后主文件看起来很简洁:



#include "lz4.h"

extern "C" {

uint32_t compress_data(uint32_t* data, uint32_t data_size, uint32_t* result) {
  uint32_t result_size = LZ4_compress(
        (const char *)(data), (char*)(result), data_size);
  return result_size;
}

uint32_t decompress_data(uint32_t* data, uint32_t data_size, uint32_t* result, uint32_t max_output_size) {
  uint32_t result_size = LZ4_uncompress_unknownOutputSize(
        (const char *)(data), (char*)(result), data_size, max_output_size);
  return result_size;
}

}


如您所见,它只是声明了外部函数(使用extern关键字),这些函数内部使用lz4从库中调用了相应的方法。



一般来说,在我们的情况下,该文件没有用:您可以立即使用lz4.h的本机接口但是,在更复杂的项目中(例如,结合不同库的功能),使用这样的通用入口点列出所有使用的功能是很方便的。



接下来,我们使用已经提到的Emscripten编译器来编译代码



em++ main.cpp lz4.c -o wasm_compressor.js \
    -s EXPORTED_FUNCTIONS='["_compress_data","_decompress_data"]' \
    -s EXTRA_EXPORTED_RUNTIME_METHODS='["cwrap"]' \
    -s WASM=1 -s ALLOW_MEMORY_GROWTH=1


收到的工件数量令人震惊:



$ du -hs wasm_compressor.*
112K    wasm_compressor.js
108K    wasm_compressor.wasm


如果打开JS文件层,则可以看到类似以下内容的内容:







它包含许多不必要的东西:从注释到服务功能,其中大多数未使用。可以通过在Emscripten编译器中

添加-O2标志来纠正这种情况,它还包括js代码优化。

之后,js代码看起来更好:







客户代码



您需要以某种方式调用客户端处理程序。首先,通过加载用户提供的文件FileReader,我们将原始数据存储在一个原语中Uint8Array



var rawData = new Uint8Array(fileReader.result);


接下来,您需要将下载的数据传输到虚拟机。为此,首先我们使用_malloc方法分配所需的字节数,然后使用set方法将JS数组复制到那里。为了方便起见,让我们将此逻辑分为arrayToWasmPtr(数组)函数:




function arrayToWasmPtr(array) {
  var ptr = Module._malloc(array.length);
  Module.HEAP8.set(array, ptr);
  return ptr;
}


将数据加载到虚拟机的内存中后,您需要以某种方式从处理中调用该函数。但是如何找到这个功能呢?cwrap方法将帮助我们-其中的第一个参数指定所需函数的名称,第二个-返回类型,第三个-包含输入参数的列表。




compressDataFunction = Module.cwrap('compress_data', 'number', ['number', 'number', 'number']);


最后,您需要从虚拟机返回完成的字节。为此,我们编写了另一个函数,该函数使用方法将它们复制到JS数组中subarray



function wasmPtrToArray(ptr, length) {
  var array = new Int8Array(length);
  array.set(Module.HEAP8.subarray(ptr, ptr + length));
  return array;
}


这里 是处理传入文件的完整脚本HTML标记包含文件上传表单和wasm工件上传到此处



结果



您可以在此处试用原型



结果是使用WASM的有效备份。缺点-该技术的当前实现不允许释放虚拟机中分配的内存。当在一个会话中加载大量文件时,这会造成隐式泄漏,但是可以通过重用现有内存而不是分配新的内存来解决此问题。










All Articles