用 WasmEdge 在 WebAssembly 中运行 JavaScript

WebAssembly 最初作为“浏览器中的 JavaScript 替代品”诞生。其想法是在浏览器中安全地运行编译自 C/C++ 或 Rust 等语言的高性能应用程序。在浏览器中,WebAssembly 和 JavaScript 并列运行。

run-javascript-in-webassembly-02.png

图 1. 浏览器中的 WebAssembly 和 JavaScript

随着云中越来越多地使用 WebAssembly ,Wasm 现在是云原生应用程序的通用 runtime。与类似 Docker 的应用程序容器相比,WebAssembly runtime 以更低的资源消耗实现更高的性能。在云上,WebAssembly 常见的应用场景包括:

可是,在这些云原生应用场景中,开发者常常想要使用 JavaScript 来编写商业应用。这意味着我们要在 WebAssembly 内支持JavaScript。此外,我们应该支持在 WebAssembly runtime 中从 JavaScript 调用 C/C++ 或 Rust 函数,充分利用 WebAssembly 的计算效率。用 WasmEdge WebAssembly runtime 能够轻松做到这些。

run-javascript-in-webassembly-03.png

图 2. 云中的 WebAssembly 和 JavaScript

WasmEdge

WasmEdge 是一个领先的云原生 WebAssembly runtime,由 CNCF(云原生计算基金会) / Linux 基金会托管 。它是当今市场上性能非常好的 WebAssembly runtime。 WasmEdge 支持所有标准的 WebAssembly 扩展以及 Tensorflow 推理、networking、KV 存储和图像处理等的专有扩展。其编译器工具链不仅支持 WebAssembly 语言,如 C/C++、Rust、Swift、Kotlin 和 AssemblyScript,还支持常规 JavaScript

WasmEdge 应用可以嵌入到 C 程序、 Go 程序、 Rust 程序、 JavaScript 程序,或者操作系统的 CLI 中。WasmEdge 可以由以下工具管理:

现在,你可以在由 WasmEdge 支持的 serverless 函数、微服务和 AIoT 应用程序中运行 JavaScript 程序!WasmEdge 不仅可以运行普通的 JavaScript 程序,而且还允许开发者使用 Rust 和 C/C++ 在 WebAssembly 的安全沙箱中创建新的 JavaScript API。

在 WasmEdge 中构建 JavaScript 引擎

首先,让我们为 WasmEdge 构建一个基于 WebAssembly 的 JavaScript 解释器程序。这个程序基于 QuickJS ,带有 WasmEdge 扩展,例如 network socketsTensorflow 推理,并且作为 JavaScript API 被合并到解释器中。 首先,需要安装 Rust 来构建解释器。

如果你只想使用解释器来运行 JavaScript 程序,你可以跳过这个部分。确保你已经安装了 RustWasmEdge

Fork 或 clone wasmedge-quickjs Github repo 来开始。

$ git clone https://github.com/second-state/wasmedge-quickjs
复制代码

按照 repo 中的说明,你将能够为 WasmEdge 构建 JavaScript 解释器。

# Install GCC
$ sudo apt update
$ sudo apt install build-essential

# Install wasm32-wasi target for Rust
$ rustup target add wasm32-wasi

# Build the QuickJS JavaScript interpreter
$ cargo build --target wasm32-wasi --release
复制代码

基于 WebAssembly 的 JavaScript 解释器程序位于 build target 目录中。你现在可以尝试一个简单的 “hello world” JavaScript 程序 (example_js/hello.js),它会打印出命令行参数到控制器。

args = args.slice(1)
print("Hello", ...args)
复制代码

在 WasmEdge 的 QuickJS runtime 运行 hello.js 文件,如下。注意,命令行中的 --dir .:. 是要准许 wasmedge 读取文件系统中 hello.js文件的本地目录。

$ cd example_js
$ wasmedge --dir .:. ../target/wasm32-wasi/release/wasmedge_quickjs.wasm hello.js WasmEdge Runtime
Hello WasmEdge Runtime
复制代码

ES6 模块支持

WasmEdge QuickJS 运行时支持 ES6 模块。GitHub repo 的 example_js/es6_module_demo 文件夹含有一个实例。 module_def.js 文件夹定义和输出一个简单的 JS 函数。

function hello(){
    console.log('hello from module_def.js')
}

export {hello}
复制代码

module_def_async.js 文件夹定义和输出一个 aysnc 函数和一个变量。

export async function hello(){
    console.log('hello from module_def_async.js')
    return "module_def_async.js : return value"
}

export var something = "async thing"
复制代码

demo.js 文件从这些模块中输入函数和变量并执行他们。

import { hello as module_def_hello } from './module_def.js'
module_def_hello()

var f = async ()=>{
    let {hello , something} = await import('./module_def_async.js')
    await hello()
    console.log("./module_def_async.js `something` is ",something)
}
f()
复制代码

要想运行这个案例,你可以在 CLI 中进行如下操作。

$ cd example_js/es6_module_demo
$ wasmedge --dir .:. ../../target/wasm32-wasi/release/wasmedge_quickjs.wasm demo.js
hello from module_def.js
hello from module_def_async.js
./module_def_async.js `something` is  async thing
复制代码

CommonJS 支持

WasmEdge QuickJS runtime 支持 CommonJS (CJS) 模块。GitHub repo中的 example_js/simple_common_js_demo 文件夹含有几个示例:

other_module/main.js 文件定义和输出一个简单的 CJS 模块。

print('hello other_module')
module.exports = ['other module exports']
复制代码

one_module/main.js 文件使用 CJS 模块。

print('hello one_module');
print('dirname:',__dirname);
let other_module_exports = require('../other_module/main.js')
print('other_module_exports=',other_module_exports)
复制代码

接下来 file_module.js 文件输入该模块并运行它。

import * as one from './one_module/main.js'
print('hello file_module')
复制代码

要运行该示例,你需要创建一个带有 CJS 支持的 WasmEdge QuickJS runtime。

$ cargo build --target wasm32-wasi --release --features=cjs
复制代码

最后,在 CLI 上进行如下操作。

$ cd example_js/simple_common_js_demo
$ wasmedge --dir .:. ../../target/wasm32-wasi/release/wasmedge_quickjs.wasm file_module.js
hello one_module
dirname: one_module
hello other_module
other_module_exports= other module exports
hello file_module
复制代码

NodeJS 模块支持

有了 CommonJS 支持,我们也能在 WasmEdge 中运行 NodeJS 模块。 simple_common_js_demo/npm_main.js demo 显示了它如何工作。它利用第三方 md5mathjs 模块。

import * as std from 'std'

var md5 = require('md5');
console.log(__dirname);
console.log('md5(message)=',md5('message'));
const { sqrt } = require('mathjs')
console.log('sqrt(-4)=',sqrt(-4).toString())

print('write file')
let f = std.open('hello.txt','w')
let x = f.puts("hello wasm")
f.flush()
f.close()
复制代码

为了运行它,我们需要使用 vercel ncc 工具来构建所有依赖相为一个单一文件。构建脚本是 package.json

{
  "dependencies": {
    "mathjs": "^9.5.1",
    "md5": "^2.3.0"
  },
  "devDependencies": {
    "@vercel/ncc": "^0.28.6"
  },
  "scripts": {
    "ncc_build": "ncc build npm_main.js"
  }
}
复制代码

现在通过 NPM 安装 nccnpm_main.js 依赖项,然后在 dist/index.js 中构建单个 JS 文件。

$ npm install
$ npm run ncc_build
ncc: Version 0.28.6
ncc: Compiling file index.js
复制代码

在 WasmEdge CLI 运行下面的命令,来运行 NodeJS 输出中的 JS 文件。

$ wasmedge --dir .:. ../../target/wasm32-wasi/release/wasmedge_quickjs.wasm dist/index.js
dist
md5(message)= 78e731027d8fd50ed642340b7c9a63b3
sqrt(-4)= 2i
write file
复制代码

接下来让我们尝试一些更高级的 JavaScript 程序。

JavaScript networking 客户端示例

解释器支持 WasmEdge networking socket 扩展,以便 JavaScript 程序可以建立到互联网的 HTTP 连接。这是一个 JavaScript 示例。

let r = GET("http://18.235.124.214/get?a=123",{"a":"b","c":[1,2,3]})
print(r.status)
    
let headers = r.headers
print(JSON.stringify(headers))

let body = r.body;
let body_str = new Uint8Array(body)
print(String.fromCharCode.apply(null,body_str))
复制代码

在 WasmEdge runtime 运行 JavaScript ,在 CLI 运行下面的命令。

$ cd example_js
$ wasmedge --dir .:. ../target/wasm32-wasi/release/wasmedge_quickjs.wasm http_demo.js
复制代码

你应该能在控制器中看到打印出的 HTTP GET 结果。

一个 JavaScript networking server 示例

下面是运行在端口 3000 侦听的 HTTP 服务器的 JavaScript 示例。

import {HttpServer} from 'http'

let http_server = new HttpServer('0.0.0.0:8000')
print('listen on 0.0.0.0:8000')

while(true){
    http_server.accept((request)=>{
        let body = request.body
        let body_str = String.fromCharCode.apply(null,new Uint8Array(body))
        print(JSON.stringify(request),'\n body_str:',body_str)

        return {
            status:200,
            header:{'Content-Type':'application/json'},
            body:'echo:'+body_str
        }
    });
}

复制代码

要在 WasmEdge 运行时中运行 JavaScript,你可以在 CLI 上执行此操作。 由于它是一个服务器,你应该在后台运行它。

$ cd example_js
$ nohup wasmedge --dir .:. ../target/wasm32-wasi/release/wasmedge_quickjs.wasm http_server_demo.js &
复制代码

然后你可以通过网络查询来测试服务器。

$ curl -d "WasmEdge" -X POST http://localhost:8000
echo:WasmEdge
复制代码

你现在应该会在控制台上看到打印的 HTTP POST body。

JavaScript Tensorflow 推理示例

解释器支持 WasmEdge Tensorflow lite 推理扩展,以便 JavaScript 可以运行 ImageNet 模型进行图像分类。 这是 JavaScript 的示例。

import {TensorflowLiteSession} from 'tensorflow_lite'
import {Image} from 'image'

let img = new Image('./example_js/tensorflow_lite_demo/food.jpg')
let img_rgb = img.to_rgb().resize(192,192)
let rgb_pix = img_rgb.pixels()

let session = new TensorflowLiteSession('./example_js/tensorflow_lite_demo/lite-model_aiy_vision_classifier_food_V1_1.tflite')
session.add_input('input',rgb_pix)
session.run()
let output = session.get_output('MobilenetV1/Predictions/Softmax');
let output_view = new Uint8Array(output)
let max = 0;
let max_idx = 0;
for (var i in output_view){
    let v = output_view[i]
    if(v>max){
        max = v;
        max_idx = i;
    }
}
print(max,max_idx)
复制代码

在 WasmEdge runtime 运行 JavaScript ,你可以在 CLI 上执行以下操作,使用 Tensorflow 重新构建 QuickJS 引擎,然后使用 Tensorflow API 运行 JavaScript 程序。

$ cargo build --target wasm32-wasi --release --features=tensorflow
... ...
$ wasmedge-tensorflow-lite --dir .:. target/wasm32-wasi/release/quickjs-rs-wasi.wasm example_js/tensorflow_lite_demo/main.js
label:
Hot dog
confidence:
0.8941176470588236
复制代码

注意:

  • --features=tensorflow 编译器标志使用 WasmEdge Tensorflow 扩展构建一个 QuickJS 引擎版本。
  • wasmedge-tensorflow-lite 程序是 WasmEdge 包的一部分。它是内置 Tensorflow 扩展的 WasmEdge runtime

现在应该可以看到 TensorFlow lite ImageNet 模型识别的食品名称。

--features=tensorflow 编译器标志使用 WasmEdge Tensorflow 扩展构建 QuickJS 引擎的一个版本。 wasmedge-tensorflow-lite 程序是 WasmEdge 软件包的一部分。 它是内置 Tensorflow 扩展的 WasmEdge 运行时。

进一步加速

上面的 Tensorflow 推理示例需要 1-2 秒才能运行。这在 Web 应用场景中是可以接受的,但还能改进。回想一下,由于其 AOT(ahead of time)优化,WasmEdge 是当今最快的 WebAssembly Runtime。 WasmEdge 提供了一个 wasmedgec 实用程序来将 wasm 文件编译为原生的 so 共享库。你可以使用 wasmedge 来运行 so 文件而不是 wasm 文件以获得更快的性能。

下面的例子使用了 wasmedgewasmedgec 的扩展版本来支持 WasmEdge Tensorflow 扩展。

$ wasmedgec-tensorflow target/wasm32-wasi/release/quickjs-rs-wasi.wasm quickjs-rs-wasi.so
$ wasmedge-tensorflow-lite --dir .:. quickjs-rs-wasi.so example_js/tensorflow_lite_demo/main.js
label:
Hot dog
confidence:
0.8941176470588236
复制代码

这次的图像分类任务可以在0.1秒内完成。这至少是 10 倍的改进!

so 共享库不能跨机器和操作系统移植。你应该在部署和运行应用程序的机器上运行 wasmedecwasmedec-tensorflow

关于 QuickJS 的注释

选择 QuickJS 作为我们的 JavaScript 引擎可能会引发性能问题。由于缺乏 JIT 支持,QuickJS 是不是比 v8 慢很多?没错!但是……

首先,QuickJS 比 v8 小很多。事实上,它只需要 v8 消耗的 runtime 资源的 1/40(或 2.5%)。你可以在单个物理机上运行比 v8 函数多得多的 QuickJS 函数。

其次,对于大多数业务逻辑应用程序,原始性能并不重要。应用程序可能具有计算密集型任务,例如动态 AI 推理。 WasmEdge 允许 QuickJS 应用程序使用高性能 WebAssembly 来完成这些任务,而在 v8 中添加此类扩展模块并不容易。

第三,众所周知,许多 JavaScript 安全问题源于 JIT。也许在云原生环境中关闭 JIT 并不是一个坏主意!

接下来呢?

这些示例演示了如何在 WasmEdge 中使用 quickjs-rs-wasi.wasm JavaScript 引擎。 除了使用 CLI,你还可以使用 Docker / Kubernetes 工具来启动 WebAssembly 应用程序或将应用程序嵌入到你自己的应用程序或框架中,正如我们在本文前面所讨论的那样。

云原生 WebAssembly 中的 JavaScript 仍然是下一代云和边缘计算基础设施中的新兴领域。我们才刚刚开始!如果你有兴趣,欢迎加入我们的 WasmEdge项目。你也可以通过提出 feature request issue 告诉我们你的需求)。

猜你喜欢

转载自juejin.im/post/7029155731978846245
今日推荐