title: [WebAssembly 入门] 与 Webpack 联动
date: 2018-4-6 19:40:00
categories: WebAssembly, 笔记
tags: WebAssembly, JavaScript, Rust, LLVM toolchain
auther: Yiniau
与 Webpack 联动
常规的进行rust代码编写再手动编译为wasm文件是十分缓慢的,目前有几种解决方案,接下来我将基于webpack来提升WebAssembly的编写效率。
首先,webpack 4 是必须的,此文写下时的version是 webpack 4.5.0
具体的webpack安装自行解决
配置 webpack
创建一个webpack.config.js,输入如下代码
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const paths = {
src: path.resolve(__dirname, 'src'),
entryFile: path.resolve(__dirname, 'src', 'index.js'),
dist: path.resolve(__dirname, 'dist'),
wasm: path.relative(__dirname, 'build'),
}
module.exports = {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
contentBase: __dirname,
hot: true,
port: 10001,
open: true, // will open on browser after started
},
entry: paths.entryFile,
output: {
path: paths.dist,
filename: 'main.js'
},
resolve: {
alias: {
wasm: paths.wasm,
}
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
}
}],
},
],
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'WebAssembly Hello World'
}),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
],
};
配置文件解读就不做了,这是个很基础的配置,如果想要学习进阶配置可以看看create-react-app里eject出来的webpack配置文件
配置 babel
.babelrc
{
"presets": [
"env"
],
"plugins": [
"syntax-dynamic-import",
"syntax-async-functions"
]
}
这里开启了async支持和import()动态导入的支持
要注意的是,静态的import导入.wasm文件并不被webpack内置支持,webpack会在控制台打印错误信息,提示你换成动态导入(Dynamic import)
不添加社区loader支持
webpack 4 内置支持解析 .wasm 文件,并且import不写后缀时搜索的优先级最高
首先让我们看看目录的情况
𝝪 yiniau @ yiniau in /Users/yiniau/code/WebAssembly/hello_world
↪ ll
total 952
-rw-r--r-- 1 yiniau staff 52B 3 25 22:14 Cargo.lock
-rw-r--r-- 1 yiniau staff 143B 4 6 21:51 Cargo.toml
drwxr-xr-x 4 yiniau staff 128B 4 6 01:36 build
-rw-r--r-- 1 yiniau staff 12B 3 26 21:53 build.sh
-rw-r--r-- 1 yiniau staff 170B 4 4 16:26 index.html
drwxr-xr-x 809 yiniau staff 25K 4 6 20:34 node_modules
-rw-r--r-- 1 yiniau staff 782B 4 6 20:34 package.json
drwxr-xr-x 3 yiniau staff 96B 4 4 16:41 rust
drwxr-xr-x 4 yiniau staff 128B 4 4 17:16 src
drwxr-xr-x 5 yiniau staff 160B 3 29 15:29 target
-rw-r--r-- 1 yiniau staff 1.2K 4 6 15:51 webpack.config.js
-rw-r--r-- 1 yiniau staff 230K 4 6 01:07 yarn-error.log
-rw-r--r-- 1 yiniau staff 216K 4 6 16:09 yarn.lock
其中
rust中存放.rs文件src中存放.js文件build中存放.wasm文件index.js为 entry 指定的入口文件,我在这里引入polyfill
𝝪 yiniau @ yiniau in /Users/yiniau/code/WebAssembly/hello_world
↪ ll src
total 16
-rw-r--r-- 1 yiniau staff 77B 4 6 20:39 index.js
-rw-r--r-- 1 yiniau staff 1.9K 4 6 21:30 main.js
ok,让我们在main.js中完成主要的逻辑吧
main.js
(async () => {
import('../build/hello.wasm')
.then(bytes => bytes.arrayBuffer())
.then(res => WebAssembly.instantiate(bytes, imports))
.then(results => {
console.log(results);
const exports = results.instance.exports;
console.log(exports);
mem = exports.memory;
});
})()
oh heck!! 为什么会报错!!
没事,错误信息很明确
WebAssembly.Instance is disallowed on the main thread, if the buffer size is larger than 4KB. Use WebAssembly.instantiate.
如果 buffer的大小超过了 4KB,WebAssembly.Instance 在主线程中不被允许使用。需要使用WebAssembly.instantiate代替,但是问题来了。
import() 并不能传递 importsObject。让我们去 webpack 的github上找找看issue:
linclark(a cartoon to WebAssembly 的作者) 提出使用 instantiateStreaming 代替 compileStreaming,以避免在ios上因快速内存的限制造成的影响。
sokra 对此表示有点反对(应该是非常反对!)
不支持的原因
预备信息
webpack试图像ESM一样对待WASM。 将适用于ESM的所有规则/假设也应用于WASM。假设将来WASM JS API可能会被WASM集成到ESM模块图中。
这意味着WASM文件中的imports部分(importsObject)与ESM中的import语句一样被解析,exports部分(instance.exports)被视为像ESM中的export部分。
WASM模块也有一个start部分,在WASM实例化时执行。
在WASM中,JS API的导入通过importsObject传递给实例化的WASM模块。
ESM规范
ESM规范指定了多个阶段。一个阶段是ModuleEvaluation。在这个阶段,所有的模块都按照明确的顺序进行评估。这个阶段是同步的。所有模块都以相同的“tick”进行评估。
当WASM在模块图中时,这意味着:
start部分在相同的“tick”中执行- WASM的所有依赖关系都在相同的“tick”中执行
- 导入WASM的ESM在相同的“tick”中执行
对于使用Promise的instantiate,这种行为是不可能的。一个Promise总是将它的履行延迟到另一个“tick”中。
只有在使用实例化同步版本(WebAssembly.Instance)时才可能。
注意:从技术上讲,可能会有一个没有start部分并且没有依赖关系的WASM。在这种情况下,这不适用。但我们不能认为情况总是如此。
webpack想要并行下载/编译wasm文件与下载JS代码。使用instantiateStreaming不会允许这样做(当WASM具有依赖关系时),因为实例化需要传递一个importsObject。创建importsObject需要评估WASM的所有依赖项/导入,因此需要在开始下载WASM之前下载这些依赖项。
当使用compileStreaming + new WebAssembly.Instance并行下载和编译是可能的,因为compileStreaming不需要一个importsObject。可以在WASM和JS下载完成时创建importsObject。
WebAssembly规范
我也想引用WebAssembly规范。它指出编译发生在后台线程中compile和实例化发生在主线程上。
它没有明确说明,但在我看来JSC的行为是不规范的。
其他说明
WASM也缺乏使用导入标识符的实时绑定的能力。相反,importsObject将被复制。这可能会伴随奇怪的循环依赖和WASM上的问题。
在importsObject中支持getter并且能够在执行start部分之前获得exports会更好。
尝试使用loader直接解析.rs
wasm-loader 对于直接使用 rustup wasm32-unknown-unknown 编译的.wasm文件支持有问题,看了下wasm-loader使用了基于emcc工具链产出的wasm文件,我试过直接使用
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
}
}],
},
{
test: /\.wasm$/,
include: path.resolve(__dirname, 'wasm'),
use: 'wasm-loader',
},
],
但是会报错:
这应该是工具链产出的编码问题
于是我再次尝试了使用rust-native-wasm-loader:
webpack.config.js
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
}
}],
},
{
test: /\.rs$/,
include: paths.rust,
use: [{
loader: 'wasm-loader'
}, {
loader: 'rust-native-wasm-loader',
options: {
release: true,
},
},]
},
],
rust/add.rs
#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
eprintln!("add({:?}, {:?}) was called", a, b);
a + b
}
main.js
import loadAdd from 'rust/add.rs';
loadAdd().then(result => {
const add = result.instance.exports['add'];
console.log('return value was', add(2, 3));
});
BUT!
臣妾做不到啊!!
我已经完全按照rust-native-wasm-loader的例子改了,但似乎现在的插件都是在asm.js时代遗留的,都是在解析成.wasm那一步失败,是因为WebAssembly不适合以同步方式调用吗。。就目前来看,如果在rust中调用std::mem来操作Memory对象,文件大小会非常大——使用wasm-gc后依旧有200多的KB
#![feature(custom_attribute)]
#![feature(wasm_import_memory)]
#![wasm_import_memory]
use std::mem;
use std::ffi::{CString, CStr};
use std::os::raw::{c_char, c_void};
/// alloc memory
#[no_mangle]
// In order to work with the memory we expose (de)allocation methods
pub extern fn alloc(size: usize) -> *mut c_void {
let mut buf = Vec::with_capacity(size);
let ptr = buf.as_mut_ptr();
mem::forget(buf);
ptr as *mut c_void
}
或许webpack的做法并不适合全部的web assembly的应用模式,以ESM的方式处理.wasm似乎很美好,但是实际使用可能会成问题,目前主要还是js处理逻辑,为了兼容低版本浏览器使用异步处理(或许是)必须的?
2018-4-17 12:00 更新
Parcel!
webassembly的webpack支持PR有更新了!一句不起眼的tip I don't know if this helps but it seems parceljs has got support for rust functions. BY pyros2097 https://medium.com/@devongovett/parcel-v1-5-0-released-source-maps-webassembly-rust-and-more-3a6385e43b95
ok
我滚去用Parcel了...
虽然不能传imports,单函数开发的话也能用用