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,單函式開發的話也能用用