JavaScript内核与高级编程之:`JavaScript` 与 `WebAssembly` 的互操作性:`JS` 如何调用 `Wasm` 函数,以及数据类型转换。

各位靓仔靓女,晚上好!我是你们的老朋友,人称“代码小王子”的程序猿老王。今天咱们来聊聊JavaScript和WebAssembly这对“好基友”之间的故事,特别是它们是如何“眉来眼去”互通有无的。

咱们今天的主题是:JavaScript 与 WebAssembly 的互操作性:JS 如何调用 Wasm 函数,以及数据类型转换。

说白了,就是JavaScript怎么指挥WebAssembly干活,以及它们之间的数据怎么传递。这可是WebAssembly能在Web端大放异彩的关键所在。

一、WebAssembly是个啥玩意儿?

在深入互操作性之前,咱们先简单回顾一下WebAssembly。

WebAssembly(简称Wasm)是一种新型的二进制指令格式,目标是成为Web平台的汇编语言。你可以理解为一种更接近机器码的语言,因此执行效率非常高。

它不是一种编程语言,而是一种编译目标。你可以用C、C++、Rust等语言编写代码,然后编译成WebAssembly。

为啥要有WebAssembly?

因为JavaScript虽然很灵活,但执行效率相对较低。对于一些计算密集型的任务,比如图像处理、游戏引擎等,JavaScript就显得力不从心了。WebAssembly的出现,就是为了解决这个问题,让Web应用也能拥有接近原生应用的性能。

二、JS 调用 Wasm 函数的流程

好,有了WebAssembly,那JavaScript怎么才能调用它呢? 流程如下:

  1. 编译: 首先,你需要用C/C++或Rust等语言编写代码,然后用相应的工具链将其编译成WebAssembly模块(.wasm文件)。
  2. 加载: 在JavaScript中,你需要使用fetch或者XMLHttpRequest等方法加载.wasm文件。
  3. 实例化: 使用WebAssembly.instantiateStreaming()WebAssembly.instantiate()方法将WebAssembly模块实例化。实例化会创建WebAssembly模块的实例,并将导出的函数和变量暴露给JavaScript。
  4. 调用: 通过实例化的结果,你可以直接调用WebAssembly模块中导出的函数。

三、实战演练:JS 调用 Wasm 函数

咱们来写一个简单的例子,用C语言写一个加法函数,编译成WebAssembly,然后在JavaScript中调用它。

1. C语言代码 (add.c):

#include <stdio.h>

int add(int a, int b) {
  return a + b;
}

2. 编译成 WebAssembly:

你需要安装一个 WebAssembly 工具链,比如 Emscripten。使用 Emscripten 编译命令如下:

emcc add.c -s WASM=1 -s EXPORTED_FUNCTIONS="['_add']" -o add.js

这条命令会生成两个文件:add.wasmadd.jsadd.wasm是WebAssembly模块,add.js是一个JavaScript胶水代码,负责加载和实例化add.wasm

解释一下编译参数:

  • -s WASM=1: 指定编译成WebAssembly。
  • -s EXPORTED_FUNCTIONS="['_add']": 指定要导出的函数。注意,C语言函数名前面要加下划线_
  • -o add.js: 指定输出文件名。

3. JavaScript 代码 (index.html):

<!DOCTYPE html>
<html>
<head>
  <title>WebAssembly Example</title>
</head>
<body>
  <script>
    // 加载 add.js (胶水代码)
    fetch('add.wasm')
      .then(response => response.arrayBuffer())
      .then(bytes => WebAssembly.instantiate(bytes, {}))
      .then(results => {
        instance = results.instance;
        // 调用 WebAssembly 函数
        const result = instance.exports._add(10, 20);
        console.log('Result:', result); // 输出: Result: 30
      });
  </script>
</body>
</html>

代码解释:

  • fetch('add.wasm'): 使用fetch API加载add.wasm文件。
  • response.arrayBuffer(): 将响应转换为ArrayBuffer,这是WebAssembly实例化需要的格式。
  • WebAssembly.instantiate(bytes, {}): 实例化WebAssembly模块。第二个参数是一个导入对象,咱们这里暂时不需要。
  • instance.exports._add(10, 20): 调用WebAssembly模块中导出的_add函数。注意,这里也要使用C语言函数名前面的下划线。
  • console.log('Result:', result): 将结果打印到控制台。

另一种加载方式 (使用 add.js 胶水代码):

如果你使用 Emscripten 编译时生成了add.js胶水代码,你可以直接引入add.js,然后使用它提供的API来加载和调用WebAssembly模块。

<!DOCTYPE html>
<html>
<head>
  <title>WebAssembly Example</title>
</head>
<body>
  <script src="add.js"></script>
  <script>
    Module.onRuntimeInitialized = function() {
      // 调用 WebAssembly 函数
      const result = Module._add(10, 20);
      console.log('Result:', result); // 输出: Result: 30
    };
  </script>
</body>
</html>

代码解释:

  • <script src="add.js"></script>: 引入add.js胶水代码。
  • Module.onRuntimeInitialized: 胶水代码加载完成后会触发onRuntimeInitialized事件。
  • Module._add(10, 20): 使用Module对象调用WebAssembly模块中导出的_add函数。

四、数据类型转换:JS 和 Wasm 的“语言障碍”

JavaScript和WebAssembly使用不同的数据类型。JavaScript是动态类型语言,WebAssembly则更接近底层,使用静态类型。因此,在JavaScript和WebAssembly之间传递数据时,需要进行类型转换。

JavaScript 类型 WebAssembly 类型 转换方式
Number i32, i64, f32, f64 JavaScript的Number类型可以转换为WebAssembly的整数类型(i32, i64)和浮点数类型(f32, f64)。需要注意数值范围,JavaScript的Number是64位浮点数,转换为整数时可能会丢失精度。
String N/A WebAssembly本身不支持字符串类型。通常的做法是将字符串转换为UTF-8编码的字节数组,然后通过线性内存传递给WebAssembly。WebAssembly处理完后,再将结果字节数组转换回JavaScript字符串。
Boolean i32 Boolean类型可以转换为WebAssembly的i32类型,true转换为1,false转换为0。
ArrayBuffer, TypedArray 线性内存 WebAssembly的线性内存可以直接被JavaScript访问,因此ArrayBuffer和TypedArray可以直接通过线性内存传递给WebAssembly。这是WebAssembly和JavaScript之间传递大量数据的最有效方式。
Object N/A WebAssembly本身不支持Object类型。通常的做法是将Object序列化为JSON字符串,然后通过字符串传递给WebAssembly。WebAssembly处理完后,再将结果JSON字符串反序列化为JavaScript Object。但是这种方式效率较低,不推荐使用。更推荐使用ArrayBuffer和TypedArray来传递结构化数据。

1. 整数和浮点数:

JavaScript的Number类型可以转换为WebAssembly的i32i64f32f64等类型。但是需要注意数值范围,JavaScript的Number是64位浮点数,转换为整数时可能会丢失精度。

示例 (C语言):

#include <stdio.h>

float multiply(float a, float b) {
  return a * b;
}

示例 (JavaScript):

instance.exports._multiply(3.14, 2.71); // 传递浮点数

2. 字符串:

WebAssembly本身不支持字符串类型。通常的做法是将字符串转换为UTF-8编码的字节数组,然后通过线性内存传递给WebAssembly。WebAssembly处理完后,再将结果字节数组转换回JavaScript字符串。

示例 (C语言):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 获取字符串长度
int string_length(const char* str) {
  return strlen(str);
}

// 将字符串转换为大写
char* to_uppercase(const char* str) {
  int len = strlen(str);
  char* result = (char*)malloc(len + 1); // 分配内存
  if (result == NULL) {
    return NULL; // 内存分配失败
  }

  for (int i = 0; i < len; i++) {
    result[i] = toupper(str[i]); // 转换为大写
  }
  result[len] = ''; // 添加字符串结束符

  return result;
}

示例 (JavaScript):

// 将 JavaScript 字符串转换为 UTF-8 编码的字节数组
function stringToUTF8(str) {
  const encoder = new TextEncoder();
  return encoder.encode(str);
}

// 将 UTF-8 编码的字节数组转换为 JavaScript 字符串
function UTF8ToString(ptr, len) {
  const memory = new Uint8Array(instance.exports.memory.buffer, ptr, len);
  const decoder = new TextDecoder();
  return decoder.decode(memory);
}

// 调用 WebAssembly 函数
const jsString = "hello world";
const utf8Bytes = stringToUTF8(jsString);
const length = utf8Bytes.length;

// 分配 WebAssembly 内存
const wasmPtr = instance.exports._malloc(length + 1); // +1 for null terminator

// 将字节数组写入 WebAssembly 内存
const memory = new Uint8Array(instance.exports.memory.buffer);
memory.set(utf8Bytes, wasmPtr);
memory[wasmPtr + length] = 0; // Null 终止符

// 调用 WebAssembly 函数
const wasmResultPtr = instance.exports._to_uppercase(wasmPtr);
const wasmResultLength = instance.exports._string_length(wasmResultPtr);

// 从 WebAssembly 内存中读取结果字符串
const resultString = UTF8ToString(wasmResultPtr, wasmResultLength);
console.log('Result:', resultString); // 输出: Result: HELLO WORLD

// 释放 WebAssembly 内存
instance.exports._free(wasmPtr);
instance.exports._free(wasmResultPtr);

3. ArrayBuffer 和 TypedArray:

WebAssembly的线性内存可以直接被JavaScript访问,因此ArrayBufferTypedArray可以直接通过线性内存传递给WebAssembly。这是WebAssembly和JavaScript之间传递大量数据的最有效方式。

示例 (C语言):

#include <stdio.h>

// 计算数组的和
int sum_array(int* array, int length) {
  int sum = 0;
  for (int i = 0; i < length; i++) {
    sum += array[i];
  }
  return sum;
}

示例 (JavaScript):

// 创建一个 JavaScript ArrayBuffer
const buffer = new ArrayBuffer(16); // 16 字节
const intArray = new Int32Array(buffer); // 将 ArrayBuffer 转换为 Int32Array

// 初始化数组
intArray[0] = 1;
intArray[1] = 2;
intArray[2] = 3;
intArray[3] = 4;

// 获取数组的指针
const arrayPtr = Module._malloc(intArray.byteLength); // 分配 WebAssembly 内存

// 将 JavaScript ArrayBuffer 复制到 WebAssembly 内存
const memory = new Uint8Array(Module.HEAPU8.buffer);
memory.set(new Uint8Array(buffer), arrayPtr);

// 调用 WebAssembly 函数
const result = Module._sum_array(arrayPtr, intArray.length);
console.log('Result:', result); // 输出: Result: 10

// 释放 WebAssembly 内存
Module._free(arrayPtr);

五、高级技巧:使用 Embind

Embind是Emscripten提供的一个C++到JavaScript的绑定库,它可以让你更方便地在JavaScript中调用C++代码,并且自动处理类型转换。

使用Embind可以大大简化JavaScript和WebAssembly之间的互操作性。

示例 (C++):

#include <emscripten/bind.h>

using namespace emscripten;

int add(int a, int b) {
  return a + b;
}

EMSCRIPTEN_BINDINGS(my_module) {
  function("add", &add);
}

编译命令:

emcc add.cpp -s WASM=1 -s "EXPORTED_FUNCTIONS=['_main']" -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" -o add.js

示例 (JavaScript):

Module.onRuntimeInitialized = function() {
  // 调用 WebAssembly 函数
  const result = Module.ccall('add', 'number', ['number', 'number'], [10, 20]);
  console.log('Result:', result); // 输出: Result: 30
};

六、性能优化

JavaScript和WebAssembly之间的互操作性虽然强大,但也存在一些性能开销。为了获得最佳性能,你需要注意以下几点:

  • 减少跨界调用: JavaScript和WebAssembly之间的调用是有开销的,尽量减少调用次数。
  • 批量处理数据: 尽量一次性传递大量数据,而不是多次传递少量数据。
  • 使用线性内存: 使用线性内存传递ArrayBufferTypedArray,避免不必要的数据拷贝。
  • 避免字符串传递: 如果可以,尽量避免在JavaScript和WebAssembly之间传递字符串,可以使用字节数组代替。
  • 分析性能瓶颈: 使用浏览器的开发者工具分析性能瓶颈,找出需要优化的地方。

七、总结

JavaScript和WebAssembly的互操作性是WebAssembly在Web端发挥作用的关键。通过合理的类型转换和优化,你可以充分利用WebAssembly的性能优势,构建高性能的Web应用。

记住,编程就像谈恋爱,需要耐心和技巧。多尝试,多实践,你也能成为JavaScript和WebAssembly的“红娘”,让它们“幸福美满”地在一起工作!

今天的分享就到这里,希望对大家有所帮助。如果有什么问题,欢迎随时提问。感谢大家的聆听!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注