WebAssembly 最初作为“浏览器中的 JavaScript 替代品”诞生。其想法是在浏览器中安全地运行编译自 C/C++ 或 Rust 等语言的高性能应用程序。在浏览器中,WebAssembly 和 JavaScript 并列运行。
随着云中越来越多地使用 WebAssembly ,Wasm 现在是云原生应用程序的通用 runtime。与类似 Docker 的应用程序容器相比,WebAssembly runtime 以更低的资源消耗实现更高的性能。在云上,WebAssembly 常见的应用场景包括:
可是,在这些云原生应用场景中,开发者常常想要使用 JavaScript 来编写商业应用。这意味着我们要在 WebAssembly 内支持JavaScript。此外,我们应该支持在 WebAssembly runtime 中从 JavaScript 调用 C/C++ 或 Rust 函数,充分利用 WebAssembly 的计算效率。用 WasmEdge WebAssembly runtime 能够轻松做到这些。
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 构建一个基于 WebAssembly 的 JavaScript 解释器程序。这个程序基于 QuickJS ,带有 WasmEdge 扩展,例如 network sockets 和 Tensorflow 推理,并且作为 JavaScript API 被合并到解释器中。 首先,需要安装 Rust 来构建解释器。
如果你只想使用解释器来运行 JavaScript 程序,你可以跳过这个部分。确保你已经安装了 Rust和* *WasmEdge。
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
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
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
有了 CommonJS 支持,我们也能在 WasmEdge 中运行 NodeJS 模块。 simple_common_js_demo/npm_main.js demo 显示了它如何工作。它利用第三方 md5
和 mathjs
模块。
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 安装 ncc
和 npm_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 程序。
解释器支持 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 结果。
下面是运行在端口 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。
解释器支持 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
文件以获得更快的性能。
下面的例子使用了 wasmedge
和 wasmedgec
的扩展版本来支持 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
共享库不能跨机器和操作系统移植。你应该在部署和运行应用程序的机器上运行wasmedec
和wasmedec-tensorflow
。
选择 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 告诉我们你的需求)。