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 入門] 與 Webpack 聯動](https://i.iter01.com/images/29fbd92639f44e1310acc192e9390264afa5ce4fda5717a5987ad6ee763052b2.png)
沒事,錯誤資訊很明確
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的行為是不規範的。
![[WebAssembly 入門] 與 Webpack 聯動](https://i.iter01.com/images/6b47f0d5c4932ec02e4f4e2d654c9b26922eee1c6e11a5f10609814d5a36b259.png)
![[WebAssembly 入門] 與 Webpack 聯動](https://i.iter01.com/images/6099ccf465f44e07295c89e5303b420b025d83919b9e5429e5c46b87ef225b39.png)
其他說明
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',
},
],
複製程式碼
但是會報錯:
![[WebAssembly 入門] 與 Webpack 聯動](https://i.iter01.com/images/80d8f665351f9c43ed248a89ac001e364ca48839d7dbfc44447a38b5879751ac.png)
這應該是工具鏈產出的編碼問題
於是我再次嘗試了使用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!
![[WebAssembly 入門] 與 Webpack 聯動](https://i.iter01.com/images/0ec968c0234241d313d8b8152172e86b40a78391677a2a4b0b78f4f58ca04102.png)
臣妾做不到啊!!
我已經完全按照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,單函式開發的話也能用用